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..909b5488f9dd
--- /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 spawningItem, final Dispenser dispenser) {
+ super(entity, null, block, blockFace, spawningItem);
+ 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/PlaceEntityEvent.java b/paper-api/src/main/java/io/papermc/paper/event/entity/PlaceEntityEvent.java
new file mode 100644
index 000000000000..a205e0dfeb0b
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/entity/PlaceEntityEvent.java
@@ -0,0 +1,96 @@
+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;
+
+/**
+ * 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, spawn eggs 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 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 spawningItem;
+ private boolean cancelled;
+
+ protected PlaceEntityEvent(final Entity entity, final @Nullable Player player, final Block block, final BlockFace blockFace, final ItemStack spawningItem) {
+ super(entity);
+ this.player = player;
+ this.block = block;
+ this.blockFace = blockFace;
+ this.spawningItem = spawningItem;
+ }
+
+ /**
+ * {@return the player placing the entity (if available)}
+ */
+ public @Nullable Player getPlayer() {
+ return this.player;
+ }
+
+ /**
+ * {@return the block that the entity was placed on}
+ */
+ public Block getBlock() {
+ return this.block;
+ }
+
+ /**
+ * {@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 not available.
+ *
+ * @return the spawning item
+ */
+ public ItemStack getSpawningItem() {
+ return this.spawningItem.clone();
+ }
+
+ @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/org/bukkit/event/entity/EntityPlaceEvent.java b/paper-api/src/main/java/org/bukkit/event/entity/EntityPlaceEvent.java
index 236c1fa78c17..4e7bc4b84c4b 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,58 @@
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, spawn eggs 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 spawningItem
+ ) {
+ super(entity, player, block, blockFace, spawningItem);
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 +60,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-generator/src/main/java/io/papermc/generator/types/goal/MobGoalGenerator.java b/paper-generator/src/main/java/io/papermc/generator/types/goal/MobGoalGenerator.java
index b0e60d343eef..f337b37e96f0 100644
--- a/paper-generator/src/main/java/io/papermc/generator/types/goal/MobGoalGenerator.java
+++ b/paper-generator/src/main/java/io/papermc/generator/types/goal/MobGoalGenerator.java
@@ -63,7 +63,7 @@ protected TypeSpec getTypeSpec() {
.returns(ParameterizedTypeName.get(ClassName.get(GoalKey.class), type));
List> classes;
- try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistPackages("net.minecraft").scan()) {
+ try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("net.minecraft").scan()) {
classes = scanResult.getSubclasses(Goal.class.getName()).loadClasses(Goal.class);
}
diff --git a/paper-generator/src/test/java/io/papermc/generator/MobGoalConverterTest.java b/paper-generator/src/test/java/io/papermc/generator/MobGoalConverterTest.java
index 0eed5bbf4ac0..f4de92a7cf62 100644
--- a/paper-generator/src/test/java/io/papermc/generator/MobGoalConverterTest.java
+++ b/paper-generator/src/test/java/io/papermc/generator/MobGoalConverterTest.java
@@ -17,7 +17,7 @@ public class MobGoalConverterTest {
@Test
public void testBukkitMap() {
final List> classes;
- try (ScanResult scanResult = new ClassGraph().enableClassInfo().whitelistPackages(Entity.class.getPackageName()).scan()) {
+ try (ScanResult scanResult = new ClassGraph().enableClassInfo().acceptPackages(Entity.class.getPackageName()).scan()) {
classes = scanResult.getSubclasses(Mob.class.getName()).loadClasses(Mob.class);
}
diff --git a/paper-server/patches/features/0001-Moonrise-optimisation-patches.patch b/paper-server/patches/features/0001-Moonrise-optimisation-patches.patch
index 2f5fc143544b..daba11fb757d 100644
--- a/paper-server/patches/features/0001-Moonrise-optimisation-patches.patch
+++ b/paper-server/patches/features/0001-Moonrise-optimisation-patches.patch
@@ -28453,7 +28453,7 @@ index c974b6cafb1f6aa2a57cfdc8a39c887f02f42b1d..ec40f02032f965f548b0c0a29aa9d9bb
// Paper start - PlayerChunkLoadEvent
if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) {
diff --git a/net/minecraft/server/network/config/PrepareSpawnTask.java b/net/minecraft/server/network/config/PrepareSpawnTask.java
-index 83af9ee3ba150da85b9b694cd76a5fabb5b2d8ef..1fb40837bd02672850ec9adc2797190df22b33fc 100644
+index b5d993095aa77bb132c513f73dbf6d2afb2e3943..8b318dd8aa918284c5fa89e990b13bec425102cb 100644
--- a/net/minecraft/server/network/config/PrepareSpawnTask.java
+++ b/net/minecraft/server/network/config/PrepareSpawnTask.java
@@ -171,7 +171,7 @@ public class PrepareSpawnTask implements ConfigurationTask {
@@ -28885,7 +28885,7 @@ index 1ceb7e4a3abd4d9de5133d182d3267d2164918f6..eb2fa32cff6824c14f865c8731df7d08
+ // Paper end - block counting
}
diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
-index 29896cb17fcf8ab967a937e9b0e102a8354b6889..e087d5596979044fe7fbcf7f2cccdae4e81a3d3a 100644
+index 9ab04735f1091ca73a2f6bd03368ad0f60ca6751..b52e3428e18d8580080e4185bd48901ee965aa01 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -167,7 +167,7 @@ public abstract class Entity
@@ -29136,7 +29136,7 @@ index 29896cb17fcf8ab967a937e9b0e102a8354b6889..e087d5596979044fe7fbcf7f2cccdae4
public Entity(final EntityType> type, final Level level) {
this.type = type;
-@@ -1421,34 +1527,76 @@ public abstract class Entity
+@@ -1423,34 +1529,76 @@ public abstract class Entity
}
private Vec3 collide(final Vec3 movement) {
@@ -29236,7 +29236,7 @@ index 29896cb17fcf8ab967a937e9b0e102a8354b6889..e087d5596979044fe7fbcf7f2cccdae4
}
private static float[] collectCandidateStepUpHeights(
-@@ -2738,21 +2886,110 @@ public abstract class Entity
+@@ -2740,21 +2888,110 @@ public abstract class Entity
}
public boolean isInWall() {
@@ -29358,7 +29358,7 @@ index 29896cb17fcf8ab967a937e9b0e102a8354b6889..e087d5596979044fe7fbcf7f2cccdae4
}
public InteractionResult interact(final Player player, final InteractionHand hand, final Vec3 location) {
-@@ -4363,15 +4600,17 @@ public abstract class Entity
+@@ -4365,15 +4602,17 @@ public abstract class Entity
}
public Iterable getIndirectPassengers() {
@@ -29384,7 +29384,7 @@ index 29896cb17fcf8ab967a937e9b0e102a8354b6889..e087d5596979044fe7fbcf7f2cccdae4
}
public int countPlayerPassengers() {
-@@ -4682,6 +4921,15 @@ public abstract class Entity
+@@ -4684,6 +4923,15 @@ public abstract class Entity
}
public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) {
@@ -29400,7 +29400,7 @@ index 29896cb17fcf8ab967a937e9b0e102a8354b6889..e087d5596979044fe7fbcf7f2cccdae4
if (!checkPosition(this, x, y, z)) {
return;
}
-@@ -4831,6 +5079,12 @@ public abstract class Entity
+@@ -4833,6 +5081,12 @@ public abstract class Entity
@Override
public final void setRemoved(final Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.@Nullable Cause cause) { // CraftBukkit - add Bukkit remove cause
@@ -29413,7 +29413,7 @@ index 29896cb17fcf8ab967a937e9b0e102a8354b6889..e087d5596979044fe7fbcf7f2cccdae4
org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause); // CraftBukkit
final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers
if (this.removalReason == null) {
-@@ -4841,7 +5095,7 @@ public abstract class Entity
+@@ -4843,7 +5097,7 @@ public abstract class Entity
this.stopRiding();
}
@@ -29422,7 +29422,7 @@ index 29896cb17fcf8ab967a937e9b0e102a8354b6889..e087d5596979044fe7fbcf7f2cccdae4
this.levelCallback.onRemove(reason);
this.onRemoval(reason);
// Paper start - Folia schedulers
-@@ -4875,7 +5129,7 @@ public abstract class Entity
+@@ -4877,7 +5131,7 @@ public abstract class Entity
public boolean shouldBeSaved() {
return (this.removalReason == null || this.removalReason.shouldSave())
&& !this.isPassenger()
@@ -31851,7 +31851,7 @@ index 9404f0b0b03dce54e2c9ad6ca2c36354888cc9f5..a0d7f6f8ecbc320267bdad27315fc2e2
public interface NoiseBiomeSource {
diff --git a/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java
-index 7678701a578d2dc45b35da072a0bb2c027522043..41aab9a526be77b400bed7a74614eceec95345b5 100644
+index a3fd479b67a880a513281faf5cdf51d0d0c2d963..24bc5559f11997c674fe09dae0c7828455ba66ae 100644
--- a/net/minecraft/world/level/block/Block.java
+++ b/net/minecraft/world/level/block/Block.java
@@ -366,7 +366,7 @@ public class Block extends BlockBehaviour implements ItemLike {
diff --git a/paper-server/patches/features/0005-Entity-Activation-Range-2.0.patch b/paper-server/patches/features/0005-Entity-Activation-Range-2.0.patch
index 84c65b9c31ec..904954b60524 100644
--- a/paper-server/patches/features/0005-Entity-Activation-Range-2.0.patch
+++ b/paper-server/patches/features/0005-Entity-Activation-Range-2.0.patch
@@ -462,7 +462,7 @@ index 0df8332933203a904bd9ef9efb3c9bce21e65441..1a502cbd8acea9420fa6dd8d716018b5
public void tick() {
super.tick();
diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
-index e087d5596979044fe7fbcf7f2cccdae4e81a3d3a..fea6c3b48c4eb162fbdc099fa775bc3161a4dd4e 100644
+index b52e3428e18d8580080e4185bd48901ee965aa01..1a43bbdfbd071b1ab533ef68152c988ac29d79a7 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -382,6 +382,15 @@ public abstract class Entity
@@ -495,7 +495,7 @@ index e087d5596979044fe7fbcf7f2cccdae4e81a3d3a..fea6c3b48c4eb162fbdc099fa775bc31
SynchedEntityData.Builder entityDataBuilder = new SynchedEntityData.Builder(this);
entityDataBuilder.define(DATA_SHARED_FLAGS_ID, (byte)0);
entityDataBuilder.define(DATA_AIR_SUPPLY_ID, this.getMaxAirSupply());
-@@ -1135,6 +1151,10 @@ public abstract class Entity
+@@ -1137,6 +1153,10 @@ public abstract class Entity
} else {
if (moverType == MoverType.PISTON) {
delta = this.limitPistonMovement(delta);
@@ -506,7 +506,7 @@ index e087d5596979044fe7fbcf7f2cccdae4e81a3d3a..fea6c3b48c4eb162fbdc099fa775bc31
if (delta.equals(Vec3.ZERO)) {
return;
}
-@@ -1150,6 +1170,13 @@ public abstract class Entity
+@@ -1152,6 +1172,13 @@ public abstract class Entity
this.stuckSpeedMultiplier = Vec3.ZERO;
this.setDeltaMovement(Vec3.ZERO);
}
@@ -521,7 +521,7 @@ index e087d5596979044fe7fbcf7f2cccdae4e81a3d3a..fea6c3b48c4eb162fbdc099fa775bc31
delta = this.maybeBackOffFromEdge(delta, moverType);
Vec3 movement = this.collide(delta);
diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java
-index 8b0abe9c7a3907419f6e4d55ea22fb9bc749f28b..36c0081e3e8f8776293ae619156cd0828fe5b21d 100644
+index 3dab720ea9cbe227338118d78038879ec45d456e..1549732b53affebde6af5e145e9c099e27e246e3 100644
--- a/net/minecraft/world/entity/LivingEntity.java
+++ b/net/minecraft/world/entity/LivingEntity.java
@@ -3372,6 +3372,14 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
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 8aad6f7a88e2..c38033dd87bb 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 0cfdbdee091f43ca011bc68f7479eb7b84801b09..8d9ce3d301d5f7e4106587ae00adb8dd
scopedCollector.forChild(entity.problemPath()), entity.registryAccess()
);
diff --git a/net/minecraft/world/entity/EntityType.java b/net/minecraft/world/entity/EntityType.java
-index 2cdbb5f9bace96a8c4ad7fc4de17678ec0db8a6c..6b19f8cd8655cc52a0d8deadb9ca9f24473637a4 100644
+index 21830548f9651e56c95e85d66f6612bbd5b755b4..56afd20372060ccce121e01e562045f32e776fa9 100644
--- a/net/minecraft/world/entity/EntityType.java
+++ b/net/minecraft/world/entity/EntityType.java
-@@ -1647,7 +1647,18 @@ public class EntityType implements EntityTypeTest,
+@@ -1663,7 +1663,18 @@ public class EntityType implements EntityTypeTest,
}
public static Stream loadEntitiesRecursive(final ValueInput.ValueInputList entities, final Level level, final EntitySpawnReason reason) {
diff --git a/paper-server/patches/features/0025-Optimise-EntityScheduler-ticking.patch b/paper-server/patches/features/0025-Optimise-EntityScheduler-ticking.patch
index c929667c8ef8..58a3a3a8d527 100644
--- a/paper-server/patches/features/0025-Optimise-EntityScheduler-ticking.patch
+++ b/paper-server/patches/features/0025-Optimise-EntityScheduler-ticking.patch
@@ -20,7 +20,7 @@ index 2bc436cdf5180a7943c45fabb9fbbedae6f7db56..f312a7f5b1b2a777ab36b94ce7cbf387
@Override
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
-index 8c4b40362f77cdea6e57315a6639006ac7ab27c7..a5217deb236ee922d020a7a6a03141a5f67acd77 100644
+index 72bc7f6efc8f23d6ff31d2dabb13490655c58074..9a438cbdb05c7a8c33804c3b66ff42f357a96bc3 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -1794,32 +1794,22 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop armorStandx.setYRot(direction.toYRot()), serverLevel, dispensed, null
+ armorStandx -> armorStandx.setYRot(direction.toYRot()), serverLevel, newStack, null // Paper - track changed items in the dispense event
);
++ // Paper start - Call BlockPlaceEntityEvent
++ final java.util.concurrent.atomic.AtomicBoolean cancelled = new java.util.concurrent.atomic.AtomicBoolean();
++ postSpawnConfig = postSpawnConfig.andThen(armorStand -> {
++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEntityEvent(source, armorStand, dispensed)) {
++ cancelled.set(true);
++ }
++ });
++ // Paper end - Call BlockPlaceEntityEvent
ArmorStand armorStand = EntityType.ARMOR_STAND.spawn(serverLevel, postSpawnConfig, pos, EntitySpawnReason.DISPENSER, false, false);
++ if (cancelled.get()) armorStand = null; // Paper - todo ideally should return null in the above method
if (armorStand != null) {
- dispensed.shrink(1);
+ if (shrink) dispensed.shrink(1); // Paper
@@ -77,7 +86,7 @@
this.setSuccess(true);
return dispensed;
}
-@@ -150,8 +_,45 @@
+@@ -150,8 +_,51 @@
DispensibleContainerItem bucket = (DispensibleContainerItem)dispensed.getItem();
BlockPos target = source.pos().relative(source.state().getValue(DispenserBlock.FACING));
Level level = source.level();
@@ -120,7 +129,13 @@
+
if (bucket.emptyContents(null, level, target, null)) {
- bucket.checkExtraContent(null, level, dispensed, target);
-+ bucket.checkExtraContent(null, level, dispensedItem, target); // Paper - track changed item from dispense event
++ // Paper start - Call BlockPlaceEntityEvent
++ final ItemStack finalDispensedItem = dispensedItem;
++ java.util.function.Function eventOp = mob -> {
++ return org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEntityEvent(source, mob, finalDispensedItem);
++ };
++ bucket.checkExtraContent(null, level, dispensedItem, target, eventOp); // Paper - track changed item from dispense event
++ // Paper end - Call BlockPlaceEntityEvent
return this.consumeWithRemainder(source, dispensed, new ItemStack(Items.BUCKET));
} else {
return this.defaultDispenseItemBehavior.dispense(source, dispensed);
@@ -220,7 +235,7 @@
return dispensed;
}
-@@ -242,11 +_,36 @@
+@@ -242,11 +_,41 @@
return dispensed;
} else {
BlockPos target = source.pos().relative(source.state().getValue(DispenserBlock.FACING));
@@ -251,6 +266,11 @@
+
+ PrimedTnt tnt = new PrimedTnt(level, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), null);
+ // CraftBukkit end
++ // Paper start - Call BlockPlaceEntityEvent
++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEntityEvent(source, tnt, dispensed)) {
++ return dispensed;
++ }
++ // Paper end - Call BlockPlaceEntityEvent
level.addFreshEntity(tnt);
level.playSound(null, tnt.getX(), tnt.getY(), tnt.getZ(), SoundEvents.TNT_PRIMED, SoundSource.BLOCKS, 1.0F, 1.0F);
- level.gameEvent(null, GameEvent.ENTITY_PLACE, target);
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch
index f3a2e22b82ba..78c3f468e421 100644
--- a/paper-server/patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch
@@ -1,6 +1,6 @@
--- a/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java
+++ b/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java
-@@ -58,12 +_,37 @@
+@@ -58,12 +_,42 @@
}
Vec3 spawnPos = new Vec3(spawnX, spawnY + yOffset, spawnZ);
@@ -36,6 +36,11 @@
if (minecart != null) {
- level.addFreshEntity(minecart);
- dispensed.shrink(1);
++ // Paper start - Call BlockPlaceEntityEvent
++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEntityEvent(source, minecart, dispensed)) {
++ return dispensed;
++ }
++ // Paper end - Call BlockPlaceEntityEvent
+ if (level.addFreshEntity(minecart) && shrink) dispensed.shrink(1); // Paper - if entity add was successful and supposed to shrink
+ // CraftBukkit end
}
diff --git a/paper-server/patches/sources/net/minecraft/core/dispenser/SpawnEggItemBehavior.java.patch b/paper-server/patches/sources/net/minecraft/core/dispenser/SpawnEggItemBehavior.java.patch
index 026114b948d0..10dafdb40119 100644
--- a/paper-server/patches/sources/net/minecraft/core/dispenser/SpawnEggItemBehavior.java.patch
+++ b/paper-server/patches/sources/net/minecraft/core/dispenser/SpawnEggItemBehavior.java.patch
@@ -1,6 +1,6 @@
--- a/net/minecraft/core/dispenser/SpawnEggItemBehavior.java
+++ b/net/minecraft/core/dispenser/SpawnEggItemBehavior.java
-@@ -18,14 +_,37 @@
+@@ -18,14 +_,45 @@
if (type == null) {
return dispensed;
} else {
@@ -29,7 +29,15 @@
+ // Paper end - block dispense event
try {
- type.spawn(source.level(), dispensed, null, source.pos().relative(direction), EntitySpawnReason.DISPENSER, direction != Direction.UP, false);
-+ type.spawn(source.level(), singleDispensed, null, source.pos().relative(direction), EntitySpawnReason.DISPENSER, direction != Direction.UP, false); // Paper - block dispense event - update used item stack
++ type.spawn(source.level(), singleDispensed, null, source.pos().relative(direction), EntitySpawnReason.DISPENSER, direction != Direction.UP, false, // Paper - block dispense event - update used item stack
++ // Paper start - Call BlockPlaceEntityEvent
++ org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DISPENSE_EGG,
++ entity -> {
++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEntityEvent(source, entity, dispensed)) {
++ entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
++ }
++ });
++ // Paper end - Call BlockPlaceEntityEvent
} catch (Exception var6) {
LOGGER.error("Error while dispensing spawn egg from dispenser at {}", source.pos(), var6);
return ItemStack.EMPTY;
diff --git a/paper-server/patches/sources/net/minecraft/gametest/framework/GameTestHelper.java.patch b/paper-server/patches/sources/net/minecraft/gametest/framework/GameTestHelper.java.patch
new file mode 100644
index 000000000000..e5a5e7e3f9d9
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/gametest/framework/GameTestHelper.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/gametest/framework/GameTestHelper.java
++++ b/net/minecraft/gametest/framework/GameTestHelper.java
+@@ -213,7 +_,7 @@
+ }
+
+ public void discard(final Entity entity) {
+- entity.discard();
++ entity.discard(null); // Paper
+ }
+
+ public E findOneEntity(final EntityType entityType) {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch
index cfc2d384c698..1c4748b8a8dd 100644
--- a/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch
@@ -203,7 +203,7 @@
}
public boolean removeTag(final String tag) {
-@@ -397,12 +_,18 @@
+@@ -397,12 +_,19 @@
}
public void kill(final ServerLevel level) {
@@ -212,6 +212,7 @@
this.gameEvent(GameEvent.ENTITY_DIE);
}
++ @Deprecated @io.papermc.paper.annotation.DoNotUse // CraftBukkit
public final void discard() {
- this.remove(Entity.RemovalReason.DISCARDED);
+ // CraftBukkit start - add Bukkit remove cause
@@ -273,9 +274,11 @@
@Override
public boolean equals(final Object obj) {
return obj instanceof Entity && ((Entity)obj).id == this.id;
-@@ -422,7 +_,13 @@
+@@ -421,8 +_,15 @@
+ return this.id;
}
++ @Deprecated @io.papermc.paper.annotation.DoNotUse // CraftBukkit
public void remove(final Entity.RemovalReason reason) {
- this.setRemoved(reason);
+ // CraftBukkit start - add Bukkit remove cause
@@ -1564,15 +1567,6 @@
if (from.dimension() == Level.END && to.dimension() == Level.OVERWORLD) {
for (Entity passenger : this.getPassengers()) {
if (passenger instanceof ServerPlayer player && !player.seenCredits) {
-@@ -3246,7 +_,7 @@
- }
- }
-
-- public boolean teleportTo(
-+ public final boolean teleportTo( // CraftBukkit - final
- final ServerLevel level,
- final double x,
- final double y,
@@ -3256,14 +_,30 @@
final float newXRot,
final boolean resetCamera
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 849af41b4e84..f548f84e5baa 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();
-@@ -1288,6 +_,22 @@
+@@ -1288,14 +_,45 @@
final boolean tryMoveDown,
final boolean movedUp
) {
@@ -27,13 +27,28 @@
+ final boolean movedUp,
+ final org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason createSpawnReason
+ ) {
++ return this.spawn(level, itemStack, user, spawnPos, spawnReason, tryMoveDown, movedUp, createSpawnReason, null);
++ }
++
++ public @Nullable T spawn(
++ final ServerLevel level,
++ final @Nullable ItemStack itemStack,
++ final @Nullable LivingEntity user,
++ final BlockPos spawnPos,
++ final EntitySpawnReason spawnReason,
++ final boolean tryMoveDown,
++ final boolean movedUp,
++ final org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason createSpawnReason,
++ final @Nullable Consumer super T> op
++ ) {
+ // CraftBukkit end
Consumer postSpawnConfig;
if (itemStack != null) {
postSpawnConfig = createDefaultStackConfig(level, itemStack, user);
-@@ -1295,7 +_,7 @@
+ } else {
postSpawnConfig = entity -> {};
}
++ if (op != null) postSpawnConfig = postSpawnConfig.andThen(op); // Paper
- return this.spawn(level, postSpawnConfig, spawnPos, spawnReason, tryMoveDown, movedUp);
+ return this.spawn(level, postSpawnConfig, spawnPos, spawnReason, tryMoveDown, movedUp, createSpawnReason); // CraftBukkit
@@ -69,11 +84,11 @@
+ final org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason creatureSpawnReason
+ ) {
+ return this.spawn(level, null, spawnPos, spawnReason, false, false, creatureSpawnReason);
-+ // CraftBukkit End
++ // CraftBukkit end
}
public @Nullable T spawn(
-@@ -1331,9 +_,39 @@
+@@ -1331,9 +_,40 @@
final boolean tryMoveDown,
final boolean movedUp
) {
@@ -105,6 +120,7 @@
T entity = this.create(level, postSpawnConfig, spawnPos, spawnReason, tryMoveDown, movedUp);
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/entity/LivingEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/LivingEntity.java.patch
index e6413834b5a7..9cb540a43aa5 100644
--- a/paper-server/patches/sources/net/minecraft/world/entity/LivingEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/LivingEntity.java.patch
@@ -127,13 +127,11 @@
output.putFloat("Health", this.getHealth());
output.putShort("HurtTime", (short)this.hurtTime);
output.putInt("HurtByTimestamp", this.lastHurtByMobTimestamp);
-@@ -755,7 +_,12 @@
- }
+@@ -756,6 +_,11 @@
}
-- public @Nullable ItemEntity drop(final ItemStack itemStack, final boolean randomly, final boolean thrownFromHand) {
-+ // Paper start - Extend dropItem API
-+ public final @Nullable ItemEntity drop(final ItemStack itemStack, final boolean randomly, final boolean thrownFromHand) {
+ public @Nullable ItemEntity drop(final ItemStack itemStack, final boolean randomly, final boolean thrownFromHand) {
++ // Paper start - Extend dropItem API
+ return this.drop(itemStack, randomly, thrownFromHand, true, null);
+ }
+ public @Nullable ItemEntity drop(final ItemStack itemStack, final boolean randomly, final boolean thrownFromHand, final boolean callEvent, final java.util.function.@Nullable Consumer entityOperation) {
@@ -1396,11 +1394,11 @@
}
public void onAttack() {
-+ // Paper start
-+ onAttack(null);
++ // Paper start
++ this.onAttack(null);
+ }
-+ public void onAttack(Entity entity) {
-+ // Paper end
++ public void onAttack(@Nullable Entity entity) {
++ // Paper end
}
public void detectEquipmentUpdates() {
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/Shearable.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/Shearable.java.patch
index 7b511ad56f67..a4750519682b 100644
--- a/paper-server/patches/sources/net/minecraft/world/entity/Shearable.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/Shearable.java.patch
@@ -1,6 +1,6 @@
--- a/net/minecraft/world/entity/Shearable.java
+++ b/net/minecraft/world/entity/Shearable.java
-@@ -5,7 +_,16 @@
+@@ -5,7 +_,12 @@
import net.minecraft.world.item.ItemStack;
public interface Shearable {
@@ -11,9 +11,5 @@
+
+ net.minecraft.world.level.Level level(); // Shearable API - expose default level needed for shearing.
+
-+ // Paper start - custom shear drops; ensure all implementing entities override this
-+ default java.util.List generateDefaultDrops(final ServerLevel level, final ItemStack tool) {
-+ return java.util.Collections.emptyList();
-+ }
-+ // Paper end - custom shear drops
++ java.util.List generateDefaultDrops(final ServerLevel level, final ItemStack tool); // Paper - custom shear drops;
}
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java.patch
index 291d4a74687c..4a69d54f6d24 100644
--- a/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java.patch
@@ -152,7 +152,17 @@
if (destroyedBlock) {
BlockPos randomPos = new BlockPos(
x0 + this.random.nextInt(x1 - x0 + 1), y0 + this.random.nextInt(y1 - y0 + 1), z0 + this.random.nextInt(z1 - z0 + 1)
-@@ -501,7 +_,15 @@
+@@ -493,15 +_,23 @@
+ }
+
+ @Override
+- public void knockback(final double power, final double xd, final double zd) {
++ public void knockback(final double power, final double xd, final double zd, final @Nullable Entity attacker, final io.papermc.paper.event.entity.EntityKnockbackEvent.Cause eventCause) { // Paper - knockback events
+ if (!this.phaseManager.getCurrentPhase().isSitting()) {
+- super.knockback(power, xd, zd);
++ super.knockback(power, xd, zd, attacker, eventCause); // Paper - knockback events
+ }
+ }
@Override
public void kill(final ServerLevel level) {
diff --git a/paper-server/patches/sources/net/minecraft/world/item/ArmorStandItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/ArmorStandItem.java.patch
index 2d44e2ce4371..ecae6ff163eb 100644
--- a/paper-server/patches/sources/net/minecraft/world/item/ArmorStandItem.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/ArmorStandItem.java.patch
@@ -6,7 +6,7 @@
entity.snapTo(entity.getX(), entity.getY(), entity.getZ(), yRot, 0.0F);
+ // CraftBukkit start
+ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(context, entity).isCancelled()) {
-+ if (context.getPlayer() != null) context.getPlayer().containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
++ if (context.getPlayer() != null) context.getPlayer().containerMenu.forceHeldSlot(context.getHand()); // Paper - Fix inventory desync
+ return InteractionResult.FAIL;
+ }
+ // CraftBukkit end
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 2c9c26a6af7c..ea01ae0a6c31 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, hitResult.getBlockPos(), player.getDirection(), player, boat, hand).isCancelled()) {
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(level, hitResult.getBlockPos(), player.getDirection(), player, boat, hand, itemStack).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 bcc7764e4ea5..b772c02e44ab 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,23 @@
if (!level.isClientSide()) {
CriteriaTriggers.FILLED_BUCKET.trigger((ServerPlayer)player, taken);
}
-@@ -74,7 +_,7 @@
+@@ -74,8 +_,13 @@
} else {
BlockState clicked = level.getBlockState(pos);
BlockPos placePos = clicked.getBlock() instanceof LiquidBlockContainer && this.content == Fluids.WATER ? pos : directionOffsetPos;
- if (this.emptyContents(player, level, placePos, hitResult)) {
+- this.checkExtraContent(player, level, itemStack, placePos);
+ if (this.emptyContents(player, level, placePos, hitResult, hitResult.getDirection(), pos, itemStack, hand)) { // CraftBukkit
- this.checkExtraContent(player, level, itemStack, placePos);
++ // Paper start - Call EntityPlaceEvent
++ java.util.function.Function eventOp = mob -> {
++ return !org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(level, placePos, hitResult.getDirection(), player, mob, hand, itemStack).isCancelled();
++ };
++ this.checkExtraContent(player, level, itemStack, placePos, eventOp);
++ // Paper end - Call EntityPlaceEvent
if (player instanceof ServerPlayer) {
CriteriaTriggers.PLACED_BLOCK.trigger((ServerPlayer)player, placePos, itemStack);
-@@ -91,6 +_,13 @@
+ }
+@@ -91,15 +_,26 @@
}
public static ItemStack getEmptySuccessItem(final ItemStack itemStack, final Player player) {
@@ -55,7 +62,10 @@
return !player.hasInfiniteMaterials() ? new ItemStack(Items.BUCKET) : itemStack;
}
-@@ -100,6 +_,12 @@
+- @Override
+- public void checkExtraContent(final @Nullable LivingEntity user, final Level level, final ItemStack itemStack, final BlockPos pos) {
+- }
++ // Paper - delete checkExtraContents
@Override
public boolean emptyContents(final @Nullable LivingEntity user, final Level level, final BlockPos pos, final @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..04d181e6211a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/item/DispensibleContainerItem.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/item/DispensibleContainerItem.java
++++ b/net/minecraft/world/item/DispensibleContainerItem.java
+@@ -7,8 +_,15 @@
+ import org.jspecify.annotations.Nullable;
+
+ public interface DispensibleContainerItem {
++ @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper
+ default void checkExtraContent(final @Nullable LivingEntity user, final Level level, final ItemStack itemStack, final BlockPos pos) {
+- }
++ // Paper start
++ this.checkExtraContent(user, level, itemStack, pos, null);
++ }
++
++ default void checkExtraContent(final @Nullable LivingEntity user, final Level level, final ItemStack itemStack, final BlockPos pos, final java.util.function.@Nullable Function eventOp) {
++ }
++ // Paper end
+
+ boolean emptyContents(final @Nullable LivingEntity user, final Level level, final BlockPos pos, final @Nullable BlockHitResult hitResult);
+ }
diff --git a/paper-server/patches/sources/net/minecraft/world/item/EndCrystalItem.java.patch b/paper-server/patches/sources/net/minecraft/world/item/EndCrystalItem.java.patch
index 3ec27164f56d..f3a4671b7895 100644
--- a/paper-server/patches/sources/net/minecraft/world/item/EndCrystalItem.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/item/EndCrystalItem.java.patch
@@ -6,7 +6,7 @@
crystal.setShowBottom(false);
+ // CraftBukkit start
+ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(context, crystal).isCancelled()) {
-+ if (context.getPlayer() != null) context.getPlayer().containerMenu.sendAllDataToRemote(); // Paper - Fix inventory desync
++ if (context.getPlayer() != null) context.getPlayer().containerMenu.forceHeldSlot(context.getHand()); // Paper - Fix inventory desync
+ return InteractionResult.FAIL;
+ }
+ // CraftBukkit end
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..3dbdca270c8e 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,32 @@
--- a/net/minecraft/world/item/MobBucketItem.java
+++ b/net/minecraft/world/item/MobBucketItem.java
-@@ -49,7 +_,7 @@
+@@ -28,9 +_,9 @@
+ }
+
+ @Override
+- public void checkExtraContent(final @Nullable LivingEntity user, final Level level, final ItemStack itemStack, final BlockPos pos) {
++ public void checkExtraContent(final @Nullable LivingEntity user, final Level level, final ItemStack itemStack, final BlockPos pos, final java.util.function.Function eventOp) { // Paper
+ if (level instanceof ServerLevel) {
+- this.spawn((ServerLevel)level, itemStack, pos);
++ this.spawn((ServerLevel)level, itemStack, pos, eventOp); // Paper
+ level.gameEvent(user, GameEvent.ENTITY_PLACE, pos);
+ }
+ }
+@@ -40,7 +_,7 @@
+ level.playSound(user, pos, this.emptySound, SoundSource.NEUTRAL, 1.0F, 1.0F);
+ }
+
+- private void spawn(final ServerLevel level, final ItemStack itemStack, final BlockPos spawnPos) {
++ private void spawn(final ServerLevel level, final ItemStack itemStack, final BlockPos spawnPos, final java.util.function.@Nullable Function eventOp) { // Paper
+ Mob mob = this.type.create(level, EntityType.createDefaultStackConfig(level, itemStack, null), spawnPos, EntitySpawnReason.BUCKET, true, false);
+ if (mob instanceof Bucketable bucketable) {
+ CustomData entityData = itemStack.getOrDefault(DataComponents.BUCKET_ENTITY_DATA, CustomData.EMPTY);
+@@ -49,7 +_,8 @@
}
if (mob != null) {
- level.addFreshEntityWithPassengers(mob);
++ if (eventOp != null && !eventOp.apply(mob)) return; // Paper - mob doesn't exist yet, no need to discard
+ 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 55e947834b83..0f57faf2e37c 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,28 @@
spawnerHolder.setEntityId(type, level.getRandom());
level.sendBlockUpdated(pos, blockState, blockState, Block.UPDATE_ALL);
level.gameEvent(context.getPlayer(), GameEvent.BLOCK_CHANGE, pos);
-@@ -91,7 +_,7 @@
+@@ -75,26 +_,35 @@
+ spawnPos = pos.relative(clickedFace);
+ }
+
+- return spawnMob(context.getPlayer(), itemStack, level, spawnPos, true, !Objects.equals(pos, spawnPos) && clickedFace == Direction.UP);
++ return spawnMob(context.getPlayer(), itemStack, level, spawnPos, true, !Objects.equals(pos, spawnPos) && clickedFace == Direction.UP, clickedFace, context.getHand()); // Paper - pass clickedFace and hand
+ }
+ }
+ }
+
+ private static InteractionResult spawnMob(
+- final @Nullable LivingEntity user,
++ final @Nullable Player user, // Paper - LivingEntity -> Player
+ final ItemStack itemStack,
+ final Level level,
+ final BlockPos spawnPos,
+ final boolean tryMoveDown,
+- final boolean movedUp
++ final boolean movedUp,
++ final Direction clickedFace, // Paper
++ final InteractionHand hand // Paper
+ ) {
EntityType> type = getType(itemStack);
if (type == null) {
return InteractionResult.FAIL;
@@ -16,7 +37,27 @@
+ } else if (!type.isAllowedInPeaceful(itemStack.get(DataComponents.ENTITY_DATA).getUnsafe()) && level.getDifficulty() == Difficulty.PEACEFUL) { // Paper - check peaceful override
return InteractionResult.FAIL;
} else {
- if (type.spawn((ServerLevel)level, itemStack, user, spawnPos, EntitySpawnReason.SPAWN_ITEM_USE, tryMoveDown, movedUp) != null) {
+- if (type.spawn((ServerLevel)level, itemStack, user, spawnPos, EntitySpawnReason.SPAWN_ITEM_USE, tryMoveDown, movedUp) != null) {
++ // Paper start - Call EntityPlaceEvent
++ java.util.function.Consumer op = e -> {
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPlaceEvent(level, spawnPos, clickedFace, user, e, hand, itemStack).isCancelled()) {
++ e.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
++ }
++ };
++ if (type.spawn((ServerLevel)level, itemStack, user, spawnPos, EntitySpawnReason.SPAWN_ITEM_USE, tryMoveDown, movedUp, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER_EGG, op) != null) {
++ // Paper end - Call EntityPlaceEvent
+ itemStack.consume(1, user);
+ level.gameEvent(user, GameEvent.ENTITY_PLACE, spawnPos);
+ }
+@@ -114,7 +_,7 @@
+ if (!(level.getBlockState(pos).getBlock() instanceof LiquidBlock)) {
+ return InteractionResult.PASS;
+ } else if (level.mayInteract(player, pos) && player.mayUseItemAt(pos, hitResult.getDirection(), itemStack)) {
+- InteractionResult result = spawnMob(player, itemStack, level, pos, false, false);
++ InteractionResult result = spawnMob(player, itemStack, level, pos, false, false, hitResult.getDirection(), hand); // Paper
+ if (result == InteractionResult.SUCCESS) {
+ player.awardStat(Stats.ITEM_USED.get(this));
+ }
@@ -163,7 +_,7 @@
} else {
offspring.snapTo(pos.x(), pos.y(), pos.z(), 0.0F, 0.0F);
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/Block.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/Block.java.patch
index c2808c1ed09f..b9a02e13c52d 100644
--- a/paper-server/patches/sources/net/minecraft/world/level/block/Block.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/Block.java.patch
@@ -154,7 +154,7 @@
+ // Paper start - fix drops not preventing stats/food exhaustion
+ @Deprecated @io.papermc.paper.annotation.DoNotUse
+ public void playerDestroy(final Level level, final Player player, final BlockPos pos, final BlockState state, final @Nullable BlockEntity blockEntity, final ItemStack destroyedWith) {
-+ playerDestroy(level, player, pos, state, blockEntity, destroyedWith, true, true);
++ this.playerDestroy(level, player, pos, state, blockEntity, destroyedWith, true, true);
+ }
+ // Paper end - fix drops not preventing stats/food exhaustion
+
diff --git a/paper-server/patches/sources/net/minecraft/world/level/entity/EntityAccess.java.patch b/paper-server/patches/sources/net/minecraft/world/level/entity/EntityAccess.java.patch
index d662e184a0ca..e806940b1050 100644
--- a/paper-server/patches/sources/net/minecraft/world/level/entity/EntityAccess.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/entity/EntityAccess.java.patch
@@ -1,11 +1,12 @@
--- a/net/minecraft/world/level/entity/EntityAccess.java
+++ b/net/minecraft/world/level/entity/EntityAccess.java
-@@ -18,7 +_,13 @@
+@@ -18,7 +_,14 @@
Stream extends EntityAccess> getPassengersAndSelf();
- void setRemoved(Entity.RemovalReason removalReason);
+ // CraftBukkit start - add Bukkit remove cause
++ @Deprecated @io.papermc.paper.annotation.DoNotUse
+ default void setRemoved(Entity.RemovalReason removalReason) {
+ this.setRemoved(removalReason, null);
+ }
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 7a42d1810f74..5f421c35c3a2 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
@@ -12,6 +12,7 @@
import io.papermc.paper.connection.PlayerConnection;
import io.papermc.paper.event.block.BlockLockCheckEvent;
import io.papermc.paper.event.connection.PlayerConnectionValidateLoginEvent;
+import io.papermc.paper.event.entity.BlockPlaceEntityEvent;
import io.papermc.paper.event.entity.ItemTransportingEntityValidateTargetEvent;
import io.papermc.paper.event.player.PlayerBedFailEnterEvent;
import io.papermc.paper.event.player.PlayerToggleEntityAgeLockEvent;
@@ -26,6 +27,7 @@
import net.minecraft.commands.CommandSourceStack;
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;
@@ -65,6 +67,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;
@@ -553,20 +556,33 @@ 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 level, 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 level, final BlockPos clickedPos, final Direction clickedFace, final net.minecraft.world.entity.player.@Nullable Player player, final Entity entity, final InteractionHand hand, final ItemStack spawningItem) {
+ Player bukkitPlayer = player == null ? null : (Player) player.getBukkitEntity();
org.bukkit.block.Block clickedBlock = CraftBlock.at(level, 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));
- entity.level().getCraftServer().getPluginManager().callEvent(event);
-
+ final EntityPlaceEvent event = new EntityPlaceEvent(entity.getBukkitEntity(), bukkitPlayer, clickedBlock, blockFace, CraftEquipmentSlot.getHand(hand), CraftItemStack.asBukkitCopy(spawningItem));
+ event.callEvent();
return event;
}
+ public static boolean callBlockPlaceEntityEvent(final BlockSource source, final Entity entity, final ItemStack spawningItem) {
+ final Direction fromFace = source.state().getValue(DispenserBlock.FACING);
+ return callBlockPlaceEntityEvent(source.level(), source.pos(), fromFace, entity, spawningItem);
+ }
+
+ private static boolean callBlockPlaceEntityEvent(final Level level, final BlockPos sourcePos, final Direction fromFace, final Entity entity, final ItemStack spawningItem) {
+ final Block targetBlock = CraftBlock.at(level, sourcePos.relative(fromFace));
+ final BlockFace blockFace = CraftBlock.notchToBlockFace(fromFace);
+ final org.bukkit.block.Dispenser dispenser = (org.bukkit.block.Dispenser) CraftBlockStates.getBlockState(CraftBlock.at(level, sourcePos));
+
+ final BlockPlaceEntityEvent event = new BlockPlaceEntityEvent(entity.getBukkitEntity(), targetBlock, blockFace, CraftItemStack.asBukkitCopy(spawningItem), dispenser);
+ return event.callEvent();
+ }
+
public static PlayerBucketEmptyEvent callPlayerBucketEmptyEvent(Level level, net.minecraft.world.entity.player.Player player, BlockPos changed, BlockPos clicked, Direction clickedFace, ItemStack itemInHand, InteractionHand hand) {
return (PlayerBucketEmptyEvent) CraftEventFactory.getPlayerBucketEvent(false, level, player, changed, clicked, clickedFace, itemInHand, Items.BUCKET, hand);
}
diff --git a/paper-server/src/test/java/io/papermc/paper/entity/EntitySetItemSlotSilentOverrideTest.java b/paper-server/src/test/java/io/papermc/paper/entity/EntitySetItemSlotSilentOverrideTest.java
deleted file mode 100644
index 120263fbae53..000000000000
--- a/paper-server/src/test/java/io/papermc/paper/entity/EntitySetItemSlotSilentOverrideTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package io.papermc.paper.entity;
-
-import io.github.classgraph.ClassGraph;
-import io.github.classgraph.ClassInfo;
-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.entity.LivingEntity;
-import org.bukkit.support.environment.Normal;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import static org.junit.jupiter.api.Assertions.fail;
-
-@Normal
-public class EntitySetItemSlotSilentOverrideTest {
-
- public static Stream parameters() {
- final List classInfo = new ArrayList<>();
- try (ScanResult scanResult = new ClassGraph()
- .enableClassInfo()
- .enableMethodInfo()
- .whitelistPackages("net.minecraft")
- .scan()
- ) {
- for (final ClassInfo subclass : scanResult.getSubclasses(LivingEntity.class.getName())) {
- final MethodInfoList setItemSlot = subclass.getDeclaredMethodInfo("setItemSlot");
- if (!setItemSlot.isEmpty()) {
- classInfo.add(subclass);
- }
- }
- }
- return classInfo.stream();
- }
-
- @ParameterizedTest
- @MethodSource("parameters")
- public void checkSetItemSlotSilentOverrides(ClassInfo overridesSetItemSlot) {
- final MethodInfoList setItemSlot = overridesSetItemSlot.getDeclaredMethodInfo("setItemSlot");
- for (final MethodInfo methodInfo : setItemSlot) {
- for (final MethodParameterInfo methodParameterInfo : methodInfo.getParameterInfo()) {
- if ("boolean".equals(methodParameterInfo.getTypeDescriptor().toStringWithSimpleNames())) {
- return;
- }
- }
- }
- fail(overridesSetItemSlot.getName() + " needs to override setItemSlot with the boolean silent parameter as well");
- }
-}
diff --git a/paper-server/src/test/java/io/papermc/paper/entity/ShearableDropsTest.java b/paper-server/src/test/java/io/papermc/paper/entity/ShearableDropsTest.java
deleted file mode 100644
index 5e6dfc93c86e..000000000000
--- a/paper-server/src/test/java/io/papermc/paper/entity/ShearableDropsTest.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package io.papermc.paper.entity;
-
-import io.github.classgraph.ClassGraph;
-import io.github.classgraph.ClassInfo;
-import io.github.classgraph.MethodInfoList;
-import io.github.classgraph.ScanResult;
-import java.util.ArrayList;
-import net.minecraft.world.entity.Shearable;
-import org.bukkit.support.environment.Normal;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-@Normal
-class ShearableDropsTest {
-
- static Iterable parameters() {
- try (ScanResult scanResult = new ClassGraph()
- .enableClassInfo()
- .enableMethodInfo()
- .whitelistPackages("net.minecraft")
- .scan()
- ) {
- return new ArrayList<>(scanResult.getClassesImplementing(Shearable.class.getName()));
- }
- }
-
- @ParameterizedTest
- @MethodSource("parameters")
- void checkShearableDropOverrides(final ClassInfo classInfo) {
- final MethodInfoList generateDefaultDrops = classInfo.getDeclaredMethodInfo("generateDefaultDrops");
- assertEquals(1, generateDefaultDrops.size(), classInfo.getName() + " doesn't implement Shearable#generateDefaultDrops");
- }
-}
diff --git a/paper-server/src/test/java/io/papermc/paper/entity/ShearableTest.java b/paper-server/src/test/java/io/papermc/paper/entity/ShearableTest.java
index 709b90f0cd01..6c27433d3aec 100644
--- a/paper-server/src/test/java/io/papermc/paper/entity/ShearableTest.java
+++ b/paper-server/src/test/java/io/papermc/paper/entity/ShearableTest.java
@@ -15,7 +15,7 @@
class ShearableTest {
static List> nmsShearables() {
- try (final ScanResult result = new ClassGraph().enableClassInfo().whitelistPackages("net.minecraft.world.entity").scan()) {
+ try (final ScanResult result = new ClassGraph().enableClassInfo().acceptPackages("net.minecraft.world.entity").scan()) {
return result.getClassesImplementing(Shearable.class.getName()).loadClasses(Shearable.class);
}
}
diff --git a/paper-server/src/test/java/io/papermc/paper/util/SourceOverloadsTest.java b/paper-server/src/test/java/io/papermc/paper/util/SourceOverloadsTest.java
new file mode 100644
index 000000000000..f060d5314a14
--- /dev/null
+++ b/paper-server/src/test/java/io/papermc/paper/util/SourceOverloadsTest.java
@@ -0,0 +1,222 @@
+package io.papermc.paper.util;
+
+import com.google.common.base.Preconditions;
+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.ScanResult;
+import io.papermc.paper.event.entity.EntityKnockbackEvent;
+import java.lang.constant.ClassDesc;
+import java.lang.constant.ConstantDescs;
+import java.lang.constant.MethodTypeDesc;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.item.DispensibleContainerItem;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.entity.EntityAccess;
+import org.bukkit.event.entity.EntityRemoveEvent;
+import org.bukkit.event.entity.EntityTargetEvent;
+import org.bukkit.event.player.PlayerTeleportEvent;
+import org.bukkit.support.environment.Normal;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.objectweb.asm.Type;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@Normal
+class SourceOverloadsTest {
+
+ private static final String SUPPORTED_PACKAGE = "net.minecraft";
+
+ record MethodOverload(Class> owner, String name, @Nullable MethodTypeDesc basedOn, int paramCount, ClassDesc... addedParams) {
+ MethodOverload {
+ Preconditions.checkState(owner.getPackageName().startsWith(SUPPORTED_PACKAGE));
+ }
+
+ // when there's no ambiguity and only one overload is expected you can use this
+ MethodOverload(Class> owner, String name, int paramCount, ClassDesc... addedParams) {
+ this(owner, name, null, paramCount, addedParams);
+ }
+ }
+
+ private static final MethodOverload[] OVERLOADS = new MethodOverload[] {
+ new MethodOverload(
+ EntityAccess.class,
+ "setRemoved",
+ 2,
+ EntityRemoveEvent.Cause.class.describeConstable().orElseThrow()
+ ),
+ new MethodOverload(
+ Entity.class,
+ "remove",
+ 2,
+ EntityRemoveEvent.Cause.class.describeConstable().orElseThrow()
+ ),
+ new MethodOverload(
+ Entity.class,
+ "discard",
+ 1,
+ EntityRemoveEvent.Cause.class.describeConstable().orElseThrow()
+ ),
+ new MethodOverload(
+ Entity.class,
+ "removeVehicle",
+ 1,
+ ConstantDescs.CD_boolean
+ ),
+ new MethodOverload(
+ Entity.class,
+ "removePassenger",
+ 2,
+ ConstantDescs.CD_boolean
+ ),
+ new MethodOverload(
+ Entity.class,
+ "stopRiding",
+ 1,
+ ConstantDescs.CD_boolean
+ ),
+ new MethodOverload(
+ Entity.class,
+ "teleportTo",
+ MethodTypeDesc.ofDescriptor("(Lnet/minecraft/server/level/ServerLevel;DDDLjava/util/Set;FFZ)Z"),
+ 9,
+ PlayerTeleportEvent.TeleportCause.class.describeConstable().orElseThrow()
+ ),
+ new MethodOverload(
+ Entity.class,
+ "push",
+ MethodTypeDesc.ofDescriptor("(DDD)V"),
+ 4,
+ Entity.class.describeConstable().orElseThrow(),
+ EntityKnockbackEvent.Cause.class.describeConstable().orElseThrow()
+ ),
+ new MethodOverload(
+ LivingEntity.class,
+ "onEquipItem",
+ 4,
+ ConstantDescs.CD_boolean
+ ),
+ new MethodOverload(
+ LivingEntity.class,
+ "setItemSlot",
+ 3,
+ ConstantDescs.CD_boolean
+ ),
+ new MethodOverload(
+ LivingEntity.class,
+ "drop",
+ MethodTypeDesc.ofDescriptor("(Lnet/minecraft/world/item/ItemStack;ZZ)Lnet/minecraft/world/entity/item/ItemEntity;"),
+ 5,
+ ConstantDescs.CD_boolean,
+ Consumer.class.describeConstable().orElseThrow()
+ ),
+ new MethodOverload(
+ LivingEntity.class,
+ "knockback",
+ 5,
+ EntityKnockbackEvent.Cause.class.describeConstable().orElseThrow()
+ ),
+ new MethodOverload(
+ LivingEntity.class,
+ "onAttack",
+ 1,
+ Entity.class.describeConstable().orElseThrow()
+ ),
+ new MethodOverload(
+ Mob.class,
+ "setTarget",
+ 2,
+ EntityTargetEvent.TargetReason.class.describeConstable().orElseThrow()
+ ),
+ new MethodOverload(
+ Block.class,
+ "playerDestroy",
+ 8,
+ ConstantDescs.CD_boolean,
+ ConstantDescs.CD_boolean
+ ),
+ new MethodOverload(
+ DispensibleContainerItem.class,
+ "checkExtraContent",
+ 5,
+ Function.class.describeConstable().orElseThrow()
+ ),
+ // TODO check Entity#isCollidable, LivingEntity#addEffect
+ };
+
+ public static Stream methods() {
+ final List args = new ArrayList<>();
+ try (final ScanResult scanResult = new ClassGraph()
+ .enableClassInfo()
+ .enableMethodInfo()
+ .acceptPackages(SUPPORTED_PACKAGE)
+ .scan()
+ ) {
+ for (MethodOverload overload : OVERLOADS) {
+ final ClassInfoList classes;
+ if (overload.owner().isInterface()) {
+ classes = scanResult.getClassesImplementing(overload.owner().getName());
+ } else {
+ classes = scanResult.getSubclasses(overload.owner().getName());
+ }
+ for (final ClassInfo klass : classes) {
+ if (klass.hasDeclaredMethod(overload.name())) {
+ MethodInfoList methods = klass.getDeclaredMethodInfo(overload.name());
+ if (overload.basedOn() != null) {
+ methods = matches(overload, methods);
+ if (methods.isEmpty()) {
+ continue;
+ }
+ }
+
+ args.add(Arguments.of(klass, methods, overload));
+ }
+ }
+ }
+ }
+ return args.stream();
+ }
+
+ private static MethodInfoList matches(final MethodOverload expected, final MethodInfoList methods) {
+ final MethodTypeDesc fromDesc = expected.basedOn();
+ assert fromDesc != null;
+ final MethodTypeDesc toDesc = fromDesc.insertParameterTypes(expected.paramCount() - expected.addedParams().length, expected.addedParams());
+
+ final MethodInfoList result = new MethodInfoList();
+ for (final MethodInfo method : methods) {
+ final String desc = method.getTypeDescriptorStr();
+ if (desc.equals(fromDesc.descriptorString()) || desc.equals(toDesc.descriptorString())) {
+ result.add(method);
+ }
+ }
+
+ return result;
+ }
+
+ @ParameterizedTest
+ @MethodSource("methods")
+ public void checkOverloads(final ClassInfo owner, final MethodInfoList methodList, final MethodOverload expectedOverload) {
+ assertEquals(1, methodList.size(), owner.getName() + " has multiple " + expectedOverload.name() + " methods");
+ final MethodInfo method = methodList.getFirst();
+
+ assertEquals(expectedOverload.paramCount(), method.getParameterInfo().length, owner.getName() + " doesn't have " + expectedOverload.paramCount() + " params for " + expectedOverload.name());
+
+ final Type[] currentParams = Type.getArgumentTypes(method.getTypeDescriptorStr());
+ final ClassDesc[] addedParams = expectedOverload.addedParams();
+ for (int i = 0, len = addedParams.length; i < len; i++) {
+ assertEquals(addedParams[i].descriptorString(), currentParams[expectedOverload.paramCount() - len + i].getDescriptor(), owner.getName() + " needs to change its override of " + expectedOverload.name());
+ }
+ }
+}
diff --git a/paper-server/src/test/java/io/papermc/paper/world/block/BlockPlayerDestroyOverrideTest.java b/paper-server/src/test/java/io/papermc/paper/world/block/BlockPlayerDestroyOverrideTest.java
deleted file mode 100644
index 750488cb2fa6..000000000000
--- a/paper-server/src/test/java/io/papermc/paper/world/block/BlockPlayerDestroyOverrideTest.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package io.papermc.paper.world.block;
-
-import io.github.classgraph.ClassGraph;
-import io.github.classgraph.ClassInfo;
-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.level.block.Block;
-import org.bukkit.support.environment.Normal;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-@Normal
-public class BlockPlayerDestroyOverrideTest {
-
- public static Stream parameters() {
- final List classInfo = new ArrayList<>();
- try (ScanResult scanResult = new ClassGraph()
- .enableClassInfo()
- .enableMethodInfo()
- .whitelistPackages("net.minecraft")
- .scan()
- ) {
- for (final ClassInfo subclass : scanResult.getSubclasses(Block.class.getName())) {
- final MethodInfoList playerDestroy = subclass.getDeclaredMethodInfo("playerDestroy");
- if (!playerDestroy.isEmpty()) {
- classInfo.add(subclass);
- }
- }
- }
- return classInfo.stream();
- }
-
- @ParameterizedTest
- @MethodSource("parameters")
- public void checkPlayerDestroyOverrides(ClassInfo overridesPlayerDestroy) {
- final MethodInfoList playerDestroy = overridesPlayerDestroy.getDeclaredMethodInfo("playerDestroy");
- assertEquals(1, playerDestroy.size(), overridesPlayerDestroy.getName() + " has multiple playerDestroy methods");
- final MethodInfo next = playerDestroy.iterator().next();
- final MethodParameterInfo[] parameterInfo = next.getParameterInfo();
- assertEquals("boolean", parameterInfo[parameterInfo.length - 1].getTypeDescriptor().toStringWithSimpleNames(), overridesPlayerDestroy.getName() + " needs to change its override of playerDestroy");
- }
-}
diff --git a/paper-server/src/test/java/org/bukkit/craftbukkit/inventory/MetaHandledTagsTest.java b/paper-server/src/test/java/org/bukkit/craftbukkit/inventory/MetaHandledTagsTest.java
index dd282530b357..c744bc40ce9e 100644
--- a/paper-server/src/test/java/org/bukkit/craftbukkit/inventory/MetaHandledTagsTest.java
+++ b/paper-server/src/test/java/org/bukkit/craftbukkit/inventory/MetaHandledTagsTest.java
@@ -17,7 +17,7 @@ class MetaHandledTagsTest {
@Test
public void checkAllMetasHaveHandledTags() {
try (final ScanResult result = new ClassGraph()
- .whitelistPackages("org.bukkit.craftbukkit.inventory")
+ .acceptPackages("org.bukkit.craftbukkit.inventory")
.enableClassInfo().scan()) {
final ClassInfoList subclasses = result.getSubclasses(CraftMetaItem.class.getName());
assertFalse(subclasses.isEmpty(), "found 0 sub types");
diff --git a/paper-server/src/test/java/org/bukkit/event/EntityRemoveEventTest.java b/paper-server/src/test/java/org/bukkit/event/EntityRemoveEventTest.java
deleted file mode 100644
index 53e7617591b3..000000000000
--- a/paper-server/src/test/java/org/bukkit/event/EntityRemoveEventTest.java
+++ /dev/null
@@ -1,123 +0,0 @@
-package org.bukkit.event;
-
-import static org.junit.jupiter.api.Assertions.*;
-import com.google.common.base.Joiner;
-import java.util.ArrayList;
-import java.util.List;
-import net.minecraft.world.level.entity.EntityAccess;
-import org.bukkit.support.environment.Normal;
-import org.bukkit.support.test.ClassNodeTest;
-import org.junit.jupiter.api.Disabled;
-import org.objectweb.asm.Handle;
-import org.objectweb.asm.tree.AbstractInsnNode;
-import org.objectweb.asm.tree.ClassNode;
-import org.objectweb.asm.tree.InvokeDynamicInsnNode;
-import org.objectweb.asm.tree.LineNumberNode;
-import org.objectweb.asm.tree.MethodInsnNode;
-import org.objectweb.asm.tree.MethodNode;
-
-@Normal
-@Disabled // TODO Delete this test or re-enable it with changes
-public class EntityRemoveEventTest {
-
- @ClassNodeTest(value = ClassNodeTest.ClassType.CRAFT_BUKKIT,
- excludedClasses = EntityAccess.class,
- excludedPackages = "net/minecraft/gametest/framework")
- public void testForMissing(ClassNode classNode, String name) throws ClassNotFoundException {
- List missingReason = new ArrayList<>();
-
- boolean minecraftCause = false;
- boolean bukkitCause = false;
-
- for (MethodNode methodNode : classNode.methods) {
- if (methodNode.name.equals("remove") && methodNode.desc.contains("Lnet/minecraft/world/entity/Entity$RemovalReason;")) {
- if (methodNode.desc.contains("Lorg/bukkit/event/entity/EntityRemoveEvent$Cause;")) {
- bukkitCause = true;
- } else {
- minecraftCause = true;
- }
- }
-
- LineNumberNode lastLineNumber = null;
- for (AbstractInsnNode instruction : methodNode.instructions) {
- if (instruction instanceof LineNumberNode lineNumberNode) {
- lastLineNumber = lineNumberNode;
- continue;
- }
-
- if (instruction instanceof MethodInsnNode methodInsnNode) {
- // Check for discard and remove method call
- if (this.check(methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc)) {
- // Add to list
- missingReason.add(String.format("Method name: %s, name: %s, line number: %s", methodNode.name, methodInsnNode.name, lastLineNumber.line));
- }
- } else if (instruction instanceof InvokeDynamicInsnNode dynamicInsnNode) {
- // Check for discard and remove method call
- if (!dynamicInsnNode.bsm.getOwner().equals("java/lang/invoke/LambdaMetafactory")
- || !dynamicInsnNode.bsm.getName().equals("metafactory") || dynamicInsnNode.bsmArgs.length != 3) {
- continue;
- }
-
- Handle handle = (Handle) dynamicInsnNode.bsmArgs[1];
-
- if (this.check(handle.getOwner(), handle.getName(), handle.getDesc())) {
- // Add to list
- missingReason.add(String.format("[D] Method name: %s, name: %s, line number: %s", methodNode.name, handle.getName(), lastLineNumber.line));
- }
- }
- }
- }
-
- assertTrue(missingReason.isEmpty(), String.format("""
- The class %s has Entity#discard, Entity#remove and/or Entity#setRemoved method calls, which don't have a bukkit reason.
- Please add a bukkit reason to them, if the event should not be called use null as reason.
-
- Following missing reasons where found:
- %s""", classNode.name, Joiner.on('\n').join(missingReason)));
-
- if (minecraftCause == bukkitCause) {
- return;
- }
-
- if (minecraftCause) {
- fail(String.format("""
- The class %s has the Entity#remove method override, but there is no bukkit override.
- Please add a bukkit method override, which adds the bukkit cause.
- """, classNode.name));
- return; // Will never reach ):
- }
-
- fail(String.format("""
- The class %s has the Entity#remove method override, to add a bukkit cause, but there is no normal override.
- Please remove the bukkit method override, since it is no longer needed.
- """, classNode.name));
- }
-
- private boolean check(String owner, String name, String desc) throws ClassNotFoundException {
- if (!name.equals("discard") && !name.equals("remove") && !name.equals("setRemoved")) {
- if (!this.checkExtraMethod(owner, name, desc)) {
- return false;
- }
- }
-
- if (desc.contains("Lorg/bukkit/event/entity/EntityRemoveEvent$Cause;")) {
- return false;
- }
-
- Class> ownerClass = Class.forName(owner.replace('/', '.'), false, this.getClass().getClassLoader());
- if (ownerClass == EntityAccess.class) {
- return false;
- }
-
- // Found missing discard, remove or setRemoved method call
- return EntityAccess.class.isAssignableFrom(ownerClass);
- }
-
- private boolean checkExtraMethod(String owner, String name, String desc) {
- if (owner.equals("net/minecraft/world/entity/projectile/EntityShulkerBullet")) {
- return name.equals("destroy");
- }
-
- return false;
- }
-}