diff --git a/paper-api/src/main/java/io/papermc/paper/event/entity/EntityLootGenerateEvent.java b/paper-api/src/main/java/io/papermc/paper/event/entity/EntityLootGenerateEvent.java new file mode 100644 index 000000000000..1106089cb2f0 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/entity/EntityLootGenerateEvent.java @@ -0,0 +1,104 @@ +package io.papermc.paper.event.entity; + +import org.bukkit.entity.Entity; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.bukkit.event.entity.EntityEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.loot.LootContext; +import org.bukkit.loot.LootTable; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NullMarked; +import java.util.Collection; +import java.util.List; + +/** + * Called when a {@link LootTable} is generated for an {@link Entity}. + *
+ * For example: When an entity dies, an armadillo sheds, etc. + */ +@NullMarked +public class EntityLootGenerateEvent extends EntityEvent implements Cancellable { + + private static final HandlerList HANDLER_LIST = new HandlerList(); + + private final LootTable lootTable; + private final LootContext lootContext; + private final List loot; + + private boolean cancelled; + + @ApiStatus.Internal + public EntityLootGenerateEvent(Entity entity, LootTable lootTable, LootContext lootContext, List loot) { + super(entity); + this.lootTable = lootTable; + this.lootContext = lootContext; + this.loot = loot; + } + + /** + * Get the loot table used to generate loot. + * + * @return the loot table + */ + public LootTable getLootTable() { + return this.lootTable; + } + + /** + * Get the loot context used to provide context to the loot table's loot + * generation. + * + * @return the loot context + */ + public LootContext getLootContext() { + return this.lootContext; + } + + /** + * Set the loot to be generated. {@code null} items will be treated as air. + *
+ * Note: the set collection is not the one which will be returned by + * {@link #getLoot()}. + * + * @param loot the loot to generate, {@code null} to clear all loot + */ + public void setLoot(@Nullable Collection loot) { + this.loot.clear(); + if (loot != null) { + this.loot.addAll(loot); + } + } + + /** + * Get a mutable list of all loot to be generated. + *

+ * Any items added or removed from the returned list will be reflected in + * the loot generation. {@code null} items will be treated as air. + * + * @return the loot to generate + */ + public List getLoot() { + return this.loot; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @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/world/LootGenerateEvent.java b/paper-api/src/main/java/org/bukkit/event/world/LootGenerateEvent.java index 271e1f3f9a2c..fc62c7250c9a 100644 --- a/paper-api/src/main/java/org/bukkit/event/world/LootGenerateEvent.java +++ b/paper-api/src/main/java/org/bukkit/event/world/LootGenerateEvent.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.List; +import io.papermc.paper.event.entity.EntityLootGenerateEvent; import org.bukkit.World; import org.bukkit.entity.Entity; import org.bukkit.event.Cancellable; @@ -19,8 +20,8 @@ * Called when a {@link LootTable} is generated in the world for an * {@link InventoryHolder}. *

- * This event is NOT currently called when an entity's loot table has been - * generated (use {@link EntityDeathEvent#getDrops()}), but WILL be called by + * This event is NOT called when an entity's loot table has been + * generated (use {@link EntityLootGenerateEvent} or {@link EntityDeathEvent#getDrops()}), but WILL be called by * plugins invoking * {@link LootTable#fillInventory(org.bukkit.inventory.Inventory, java.util.Random, LootContext)}. */ 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 565f83bbc7d2..5e32d4288048 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 @@ -354,7 +354,7 @@ index 0000000000000000000000000000000000000000..ce6b57eeeeb1bd652f4bb53c19dcfbc0 + } +} diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 89da20b0682708ba8bfc0d10381eb9784d80c395..b5d63d7b848c11ed322d512adeee102886cbb6fd 100644 +index 0fabe7fe4d302695505bed2cef4ce580358b8b82..9fc39230f52f4cf6b44edd34aa9542b9d500538e 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -864,6 +864,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet @@ -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 593265d78564b60bacbb4899f18a6e74bf56601d..84c664711658eb83b5ff9d4c8470fd8ec54a5473 100644 +index e5d56fdb5cfd4aa291dbdb879e478134a85c9beb..196a15d3761eab416cb519fd52c86cc100b8080a 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -382,6 +382,15 @@ public abstract class Entity @@ -521,10 +521,10 @@ index 593265d78564b60bacbb4899f18a6e74bf56601d..84c664711658eb83b5ff9d4c8470fd8e 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 f62e535b62d249dd3be19d7c1b02fae8dad31c10..6eb95b979ffe37d78fd68eb57ebe71891beb0d28 100644 +index e90339ef63c535c7a376747fcedcd49275612ae3..fdd9ed362c68932a5c3dd7856108243004636d05 100644 --- a/net/minecraft/world/entity/LivingEntity.java +++ b/net/minecraft/world/entity/LivingEntity.java -@@ -3380,6 +3380,14 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin +@@ -3398,6 +3398,14 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin protected void playAttackSound() { } @@ -818,7 +818,7 @@ index f46cca0467bb4da5511bc953ce5b43fa606bf978..445c5cafad71028171c1f26193966177 + } diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java -index cc492445307ddc08446484d272866f01646cf7b3..a5d46832d4f8a13b0bf67b523c21d5c3b44209a9 100644 +index 46ca14a186f23a05bd0e6437adcb67dc09d94d8b..00bfadc908cfa9f1600c4bcc3cd21cca579cdc4f 100644 --- a/net/minecraft/world/level/Level.java +++ b/net/minecraft/world/level/Level.java @@ -155,6 +155,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl 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 a69cad61f833..936482d2ce64 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 @@ -966,6 +966,41 @@ } protected void dropCustomDeathLoot(final ServerLevel level, final DamageSource source, final boolean killedByPlayer) { +@@ -1546,8 +_,18 @@ + builder = builder.withParameter(LootContextParams.LAST_DAMAGE_PLAYER, killerPlayer).withLuck(killerPlayer.getLuck()); + } + ++ // Paper start - EntityLootGenerateEvent ++ List drops = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); + LootParams params = builder.create(LootContextParamSets.ENTITY); +- table.getRandomItems(params, this.getLootTableSeed(), itemStackConsumer); ++ table.getRandomItems(params, this.getLootTableSeed(), drops::add); ++ ++ List bukkitLoot = drops.stream().map(CraftItemStack::asCraftMirror).collect(java.util.stream.Collectors.toCollection(java.util.ArrayList::new)); ++ io.papermc.paper.event.entity.EntityLootGenerateEvent event = new io.papermc.paper.event.entity.EntityLootGenerateEvent(this.getBukkitEntity(), table.craftLootTable, org.bukkit.craftbukkit.CraftLootTable.convertParams(params), bukkitLoot); ++ if (!event.callEvent()) { ++ return; ++ } ++ event.getLoot().stream().map(org.bukkit.craftbukkit.inventory.CraftItemStack::asNMSCopy).forEach(itemStackConsumer); ++ // Paper end + } + + public boolean dropFromEntityInteractLootTable( +@@ -1602,6 +_,14 @@ + LootTable lootTable = level.getServer().reloadableRegistries().getLootTable(key); + LootParams params = paramsBuilder.apply(new LootParams.Builder(level)); + List drops = lootTable.getRandomItems(params); ++ // Paper start - EntityLootGenerateEvent ++ List bukkitLoot = drops.stream().map(CraftItemStack::asCraftMirror).collect(java.util.stream.Collectors.toCollection(java.util.ArrayList::new)); ++ io.papermc.paper.event.entity.EntityLootGenerateEvent event = new io.papermc.paper.event.entity.EntityLootGenerateEvent(this.getBukkitEntity(), lootTable.craftLootTable, org.bukkit.craftbukkit.CraftLootTable.convertParams(params), bukkitLoot); ++ if (!event.callEvent()) { ++ return false; ++ } ++ drops = event.getLoot().stream().map(org.bukkit.craftbukkit.inventory.CraftItemStack::asNMSCopy).collect(it.unimi.dsi.fastutil.objects.ObjectArrayList.toList()); ++ // Paper end + if (!drops.isEmpty()) { + drops.forEach(stack -> consumer.accept(level, stack)); + return true; @@ -1611,9 +_,14 @@ } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java index f1a616432ea7..820a40f2ba74 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java @@ -148,6 +148,29 @@ private void setMaybe(LootParams.Builder builder, ContextKey param, T val } } + public static LootContext convertParams(net.minecraft.world.level.storage.loot.LootParams info) { + Vec3 position = info.contextMap().getOptional(LootContextParams.ORIGIN); + if (position == null) { + position = info.contextMap().getOptional(LootContextParams.THIS_ENTITY).position(); // Every vanilla context has origin or this_entity, see LootContextParamSets + } + Location location = CraftLocation.toBukkit(position, info.getLevel()); + LootContext.Builder contextBuilder = new LootContext.Builder(location); + + if (info.contextMap().has(LootContextParams.ATTACKING_ENTITY)) { + CraftEntity killer = info.contextMap().getOptional(LootContextParams.ATTACKING_ENTITY).getBukkitEntity(); + if (killer instanceof CraftHumanEntity) { + contextBuilder.killer((CraftHumanEntity) killer); + } + } + + if (info.contextMap().has(LootContextParams.THIS_ENTITY)) { + contextBuilder.lootedEntity(info.contextMap().getOptional(LootContextParams.THIS_ENTITY).getBukkitEntity()); + } + + contextBuilder.luck(info.getLuck()); + return contextBuilder.build(); + } + public static LootContext convertContext(net.minecraft.world.level.storage.loot.LootContext info) { Vec3 position = info.getOptionalParameter(LootContextParams.ORIGIN); if (position == null) {