diff --git a/paper-api/src/main/java/org/bukkit/entity/HumanEntity.java b/paper-api/src/main/java/org/bukkit/entity/HumanEntity.java index 188c8e27d..6303c68b2 100644 --- a/paper-api/src/main/java/org/bukkit/entity/HumanEntity.java +++ b/paper-api/src/main/java/org/bukkit/entity/HumanEntity.java @@ -2,10 +2,12 @@ package org.bukkit.entity; import java.util.Collection; import java.util.Set; +import java.util.function.Consumer; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.InventoryView; @@ -14,6 +16,7 @@ import org.bukkit.inventory.MainHand; import org.bukkit.inventory.Merchant; import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.meta.FireworkMeta; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -703,8 +706,115 @@ public interface HumanEntity extends LivingEntity, AnimalTamer, InventoryHolder * * @param dropAll True to drop entire stack, false to drop 1 of the stack * @return True if item was dropped successfully + * @apiNote You should instead use {@link #dropItem(EquipmentSlot, int)} or {@link #dropItem(EquipmentSlot)} with a {@link EquipmentSlot#HAND} parameter. */ - public boolean dropItem(boolean dropAll); + @ApiStatus.Obsolete(since = "1.21.4") + boolean dropItem(boolean dropAll); + + /** + * Makes the player drop all items from their inventory based on the inventory slot. + * + * @param slot the equipment slot to drop + * @return the dropped item entity, or null if the action was unsuccessful + */ + @Nullable + default Item dropItem(final int slot) { + return this.dropItem(slot, Integer.MAX_VALUE); + } + + /** + * Makes the player drop an item from their inventory based on the inventory slot. + * + * @param slot the slot to drop + * @param amount the number of items to drop from this slot. Values below one always return null + * @return the dropped item entity, or null if the action was unsuccessful + * @throws IllegalArgumentException if the slot is negative or bigger than the player's inventory + */ + @Nullable + default Item dropItem(final int slot, final int amount) { + return this.dropItem(slot, amount, false, null); + } + + /** + * Makes the player drop an item from their inventory based on the inventory slot. + * + * @param slot the slot to drop + * @param amount the number of items to drop from this slot. Values below one always return null + * @param throwRandomly controls the randomness of the dropped items velocity, where {@code true} mimics dropped + * items during a player's death, while {@code false} acts like a normal item drop. + * @param entityOperation the function to be run before adding the entity into the world + * @return the dropped item entity, or null if the action was unsuccessful + * @throws IllegalArgumentException if the slot is negative or bigger than the player's inventory + */ + @Nullable + Item dropItem(int slot, int amount, boolean throwRandomly, @Nullable Consumer entityOperation); + + /** + * Makes the player drop all items from their inventory based on the equipment slot. + * + * @param slot the equipment slot to drop + * @return the dropped item entity, or null if the action was unsuccessful + */ + @Nullable + default Item dropItem(final @NotNull EquipmentSlot slot) { + return this.dropItem(slot, Integer.MAX_VALUE); + } + + /** + * Makes the player drop an item from their inventory based on the equipment slot. + * + * @param slot the equipment slot to drop + * @param amount the amount of items to drop from this equipment slot. Values below one always return null + * @return the dropped item entity, or null if the action was unsuccessful + */ + @Nullable + default Item dropItem(final @NotNull EquipmentSlot slot, final int amount) { + return this.dropItem(slot, amount, false, null); + } + + /** + * Makes the player drop an item from their inventory based on the equipment slot. + * + * @param slot the equipment slot to drop + * @param amount The amount of items to drop from this equipment slot. Values below one always return null + * @param throwRandomly controls the randomness of the dropped items velocity, where {@code true} mimics dropped + * items during a player's death, while {@code false} acts like a normal item drop. + * @param entityOperation the function to be run before adding the entity into the world + * @return the dropped item entity, or null if the action was unsuccessful + */ + @Nullable + Item dropItem(@NotNull EquipmentSlot slot, int amount, boolean throwRandomly, @Nullable Consumer entityOperation); + + /** + * Makes the player drop any arbitrary {@link ItemStack}, independently of whether the player actually + * has that item in their inventory. + *

+ * This method modifies neither the item nor the player's inventory. + * Item removal has to be handled by the method caller. + * + * @param itemStack the itemstack to drop + * @return the dropped item entity, or null if the action was unsuccessful + */ + @Nullable + default Item dropItem(final @NotNull ItemStack itemStack) { + return this.dropItem(itemStack, false, null); + } + + /** + * Makes the player drop any arbitrary {@link ItemStack}, independently of whether the player actually + * has that item in their inventory. + *

+ * This method modifies neither the item nor the player's inventory. + * Item removal has to be handled by the method caller. + * + * @param itemStack the itemstack to drop + * @param throwRandomly controls the randomness of the dropped items velocity, where {@code true} mimics dropped + * items during a player's death, while {@code false} acts like a normal item drop. + * @param entityOperation the function to be run before adding the entity into the world + * @return the dropped item entity, or null if the action was unsuccessful + */ + @Nullable + Item dropItem(final @NotNull ItemStack itemStack, boolean throwRandomly, @Nullable Consumer entityOperation); /** * Gets the players current exhaustion level. diff --git a/paper-server/patches/sources/net/minecraft/server/level/ServerPlayer.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerPlayer.java.patch index 662414567..bfb5d7cd0 100644 --- a/paper-server/patches/sources/net/minecraft/server/level/ServerPlayer.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/level/ServerPlayer.java.patch @@ -1342,17 +1342,18 @@ } public SectionPos getLastSectionPos() { -@@ -1930,21 +_,54 @@ +@@ -1930,21 +_,55 @@ } @Override - public ItemEntity drop(ItemStack droppedItem, boolean dropAround, boolean traceItem) { -+ public ItemEntity drop(ItemStack droppedItem, boolean dropAround, boolean traceItem, boolean callEvent) { // CraftBukkit - SPIGOT-2942: Add boolean to call event ++ public ItemEntity drop(ItemStack droppedItem, boolean dropAround, boolean traceItem, boolean callEvent, @Nullable java.util.function.Consumer entityOperation) { // Paper start - Extend HumanEntity#dropItem API ItemEntity itemEntity = this.createItemStackToDrop(droppedItem, dropAround, traceItem); if (itemEntity == null) { return null; } else { + // CraftBukkit start - fire PlayerDropItemEvent ++ if (entityOperation != null) entityOperation.accept((org.bukkit.entity.Item) itemEntity.getBukkitEntity()); + if (callEvent) { + org.bukkit.entity.Player player = this.getBukkitEntity(); + org.bukkit.entity.Item drop = (org.bukkit.entity.Item) itemEntity.getBukkitEntity(); diff --git a/paper-server/patches/sources/net/minecraft/world/entity/player/Player.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/player/Player.java.patch index 4d20a5178..83e1b14b2 100644 --- a/paper-server/patches/sources/net/minecraft/world/entity/player/Player.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/entity/player/Player.java.patch @@ -112,16 +112,21 @@ this.removeEntitiesOnShoulder(); } } -@@ -717,6 +_,13 @@ +@@ -717,6 +_,18 @@ @Nullable public ItemEntity drop(ItemStack droppedItem, boolean dropAround, boolean includeThrowerName) { + // CraftBukkit start - SPIGOT-2942: Add boolean to call event -+ return this.drop(droppedItem, dropAround, includeThrowerName, true); ++ return this.drop(droppedItem, dropAround, includeThrowerName, true, null); + } + + @Nullable + public ItemEntity drop(ItemStack droppedItem, boolean dropAround, boolean includeThrowerName, boolean callEvent) { ++ return this.drop(droppedItem, dropAround, includeThrowerName, callEvent, null); ++ } ++ ++ @Nullable ++ public ItemEntity drop(ItemStack droppedItem, boolean dropAround, boolean includeThrowerName, boolean callEvent, @Nullable java.util.function.Consumer entityOperation) { + // CraftBukkit end if (!droppedItem.isEmpty() && this.level().isClientSide) { this.swing(InteractionHand.MAIN_HAND); diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java index e345cdbfa..d142009c0 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java @@ -7,6 +7,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; @@ -19,6 +20,7 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.HumanoidArm; +import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.projectile.FireworkRocketEntity; import net.minecraft.world.inventory.AbstractContainerMenu; @@ -48,13 +50,14 @@ import org.bukkit.craftbukkit.inventory.CraftInventoryView; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.inventory.CraftMerchantCustom; import org.bukkit.craftbukkit.inventory.CraftRecipe; -import org.bukkit.craftbukkit.util.CraftChatMessage; import org.bukkit.craftbukkit.util.CraftLocation; import org.bukkit.entity.Firework; import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Item; import org.bukkit.entity.Villager; import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryView; import org.bukkit.inventory.ItemStack; @@ -66,6 +69,8 @@ import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionAttachment; import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity { private CraftInventoryPlayer inventory; @@ -801,6 +806,47 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity { // Paper end - Fix HumanEntity#drop not updating the client inv } + @Override + @Nullable + public Item dropItem(final int slot, final int amount, final boolean throwRandomly, final @Nullable Consumer entityOperation) { + Preconditions.checkArgument(slot >= 0 && slot < this.inventory.getSize(), "Slot %s is not a valid inventory slot.", slot); + + return internalDropItemFromInventory(this.inventory.getItem(slot), amount, throwRandomly, entityOperation); + } + + @Override + @Nullable + public Item dropItem(final @NotNull EquipmentSlot slot, final int amount, final boolean throwRandomly, final @Nullable Consumer entityOperation) { + return internalDropItemFromInventory(this.inventory.getItem(slot), amount, throwRandomly, entityOperation); + } + + @Nullable + private Item internalDropItemFromInventory(final ItemStack originalItemStack, final int amount, final boolean throwRandomly, final @Nullable Consumer entityOperation) { + if (originalItemStack == null || originalItemStack.isEmpty() || amount <= 0) return null; + + final net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.unwrap(originalItemStack); + final net.minecraft.world.item.ItemStack dropContent = nmsItemStack.split(amount); + + // This will return the itemstack back to its original amount in case events fail + final ItemEntity droppedEntity = this.getHandle().drop(dropContent, throwRandomly, true, true, entityOperation); + return droppedEntity == null ? null : (Item) droppedEntity.getBukkitEntity(); + } + + @Override + @Nullable + public Item dropItem(final @Nullable ItemStack itemStack, final boolean throwRandomly, final @Nullable Consumer entityOperation) { + // This method implementation differs from the previous dropItem implementations, as it does not source + // its itemstack from the players inventory. As such, we cannot reuse #internalDropItemFromInventory. + Preconditions.checkArgument(itemStack != null, "Cannot drop a null itemstack"); + if (itemStack.isEmpty()) return null; + + final net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack); + + // Do *not* call the event here, the item is not in the player inventory, they are not dropping it / do not need recovering logic (which would be a dupe). + final ItemEntity droppedEntity = this.getHandle().drop(nmsItemStack, throwRandomly, true, false, entityOperation); + return droppedEntity == null ? null : (Item) droppedEntity.getBukkitEntity(); + } + @Override public float getExhaustion() { return this.getHandle().getFoodData().exhaustionLevel;