From b7b9db1ac4799a9d583580239d82aafe075df6b8 Mon Sep 17 00:00:00 2001 From: masmc05 Date: Sun, 26 Jan 2025 20:55:09 +0100 Subject: [PATCH 1/5] Add Player#give --- .../paper/entity/PlayerGiveResult.java | 36 +++++++++++++++++++ .../main/java/org/bukkit/entity/Player.java | 36 +++++++++++++++++++ .../paper/entity/PaperPlayerGiveResult.java | 20 +++++++++++ .../craftbukkit/entity/CraftPlayer.java | 33 +++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 paper-api/src/main/java/io/papermc/paper/entity/PlayerGiveResult.java create mode 100644 paper-server/src/main/java/io/papermc/paper/entity/PaperPlayerGiveResult.java diff --git a/paper-api/src/main/java/io/papermc/paper/entity/PlayerGiveResult.java b/paper-api/src/main/java/io/papermc/paper/entity/PlayerGiveResult.java new file mode 100644 index 000000000000..9972f3edbdcf --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/entity/PlayerGiveResult.java @@ -0,0 +1,36 @@ +package io.papermc.paper.entity; + +import java.util.Collection; +import org.bukkit.entity.Item; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Unmodifiable; +import org.jspecify.annotations.NullMarked; + +/** + * A result type used by {@link org.bukkit.entity.Player#give(ItemStack...)} and its overloads. + */ +@NullMarked +public interface PlayerGiveResult { + + /** + * A collection of itemstacks that were not added to the player's inventory as they did not fit. + * The collection is derived from the collections of items to add by creating copies of each stack that was not + * fully added to the inventory and assigning the non-added count as their amount. + * + * @return the unmodifiable collection of itemstacks that are leftover as they could not be added. Each element is a + * copy of the input stack they are derived from. + */ + @Unmodifiable + Collection leftovers(); + + /** + * A collection of item entities dropped as a result of this call to {@link org.bukkit.entity.Player#give(ItemStack...)}. + * The item entities contained here are not guaranteed to match the {@link #leftovers()} as plugins may cancel the + * spawning of item entities. + * + * @return the unmodifiable collection of dropped item entities. + */ + @Unmodifiable + Collection drops(); + +} diff --git a/paper-api/src/main/java/org/bukkit/entity/Player.java b/paper-api/src/main/java/org/bukkit/entity/Player.java index f563cf486eea..dd0438516872 100644 --- a/paper-api/src/main/java/org/bukkit/entity/Player.java +++ b/paper-api/src/main/java/org/bukkit/entity/Player.java @@ -6,10 +6,12 @@ import java.time.Instant; import java.util.Collection; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; import io.papermc.paper.entity.LookAnchor; +import io.papermc.paper.entity.PlayerGiveResult; import org.bukkit.BanEntry; import org.bukkit.DyeColor; import org.bukkit.Effect; @@ -3892,4 +3894,38 @@ default boolean isChunkSent(@NotNull org.bukkit.Chunk chunk) { */ void sendEntityEffect(org.bukkit.@NotNull EntityEffect effect, @NotNull Entity target); // Paper end - entity effect API + + /** + * Gives the player the items following full vanilla logic, + * making the player drop the items that did not fit into + * the inventory. + * + * @param items the items to give. + * @return the result of this method, holding leftovers and spawned items. + */ + default @NotNull PlayerGiveResult give(@NotNull final ItemStack @NotNull ... items) { + return this.give(List.of(items)); + } + + /** + * Gives the player those items following full vanilla logic, + * making the player drop the items that did not fit into + * the inventory. + * + * @param items the items to give + * @return the result of this method, holding leftovers and spawned items. + */ + default @NotNull PlayerGiveResult give(@NotNull final Collection<@NotNull ItemStack> items) { + return this.give(items, true); + } + + /** + * Gives the player those items following full vanilla logic. + * + * @param items the items to give + * @param dropIfFull whether the player should drop items that + * did not fit the inventory + * @return the result of this method, holding leftovers and spawned items. + */ + @NotNull PlayerGiveResult give(@NotNull Collection<@NotNull ItemStack> items, boolean dropIfFull); } diff --git a/paper-server/src/main/java/io/papermc/paper/entity/PaperPlayerGiveResult.java b/paper-server/src/main/java/io/papermc/paper/entity/PaperPlayerGiveResult.java new file mode 100644 index 000000000000..199c58718520 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/entity/PaperPlayerGiveResult.java @@ -0,0 +1,20 @@ +package io.papermc.paper.entity; + +import java.util.Collection; +import java.util.Collections; +import org.bukkit.entity.Item; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Unmodifiable; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public record PaperPlayerGiveResult( + @Unmodifiable Collection leftovers, + @Unmodifiable Collection drops +) implements PlayerGiveResult { + + public static final PlayerGiveResult EMPTY = new PaperPlayerGiveResult( + Collections.emptyList(), Collections.emptyList() + ); + +} diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index 47d6e61bd429..30c33891e5a7 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -1,6 +1,7 @@ package org.bukkit.craftbukkit.entity; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.io.BaseEncoding; import com.mojang.authlib.GameProfile; @@ -8,6 +9,8 @@ import io.netty.buffer.Unpooled; import io.papermc.paper.FeatureHooks; import io.papermc.paper.entity.LookAnchor; +import io.papermc.paper.entity.PaperPlayerGiveResult; +import io.papermc.paper.entity.PlayerGiveResult; import it.unimi.dsi.fastutil.shorts.ShortArraySet; import it.unimi.dsi.fastutil.shorts.ShortSet; import java.io.ByteArrayOutputStream; @@ -21,6 +24,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -101,6 +105,7 @@ import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.AttributeMap; import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.food.FoodData; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.level.GameType; @@ -176,6 +181,7 @@ import org.bukkit.craftbukkit.util.CraftNamespacedKey; import org.bukkit.entity.EnderPearl; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Item; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerExpCooldownChangeEvent; @@ -3545,4 +3551,31 @@ public void sendEntityEffect(final org.bukkit.EntityEffect effect, final org.buk this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundEntityEventPacket(((CraftEntity) target).getHandle(), effect.getData())); } // Paper end - entity effect API + + @Override + public @NotNull PlayerGiveResult give(@NotNull final Collection<@NotNull ItemStack> items, final boolean dropIfFull) { + Preconditions.checkNotNull(items, "items cannot be null"); + if (items.isEmpty()) return PaperPlayerGiveResult.EMPTY; // Early opt out for empty input. + + final ServerPlayer handle = this.getHandle(); + final ImmutableList.Builder drops = ImmutableList.builder(); + final ImmutableList.Builder leftovers = ImmutableList.builder(); + for (final ItemStack item : items) { + Preconditions.checkNotNull(item, "ItemStack cannot be null"); + Preconditions.checkArgument(!item.isEmpty(), "ItemStack cannot be empty"); + Preconditions.checkArgument(item.getAmount() <= item.getMaxStackSize(), "ItemStack amount cannot be greater than its max stack size"); + final net.minecraft.world.item.ItemStack nmsStack = CraftItemStack.asNMSCopy(item); + final boolean added = handle.getInventory().add(nmsStack); + if (added && nmsStack.isEmpty()) continue; // Item was fully added, neither a drop nor a leftover is needed. + + leftovers.add(CraftItemStack.asBukkitCopy(nmsStack)); // Insert copy to avoid mutation to the dropped item from affecting leftovers + if (!dropIfFull) continue; + + final ItemEntity entity = handle.drop(nmsStack, false, true, false); + if (entity != null) drops.add((Item) entity.getBukkitEntity()); + } + + handle.containerMenu.broadcastChanges(); + return new PaperPlayerGiveResult(leftovers.build(), drops.build()); + } } From 713ccb8ee3216e3c99146f37efe7cf7021862446 Mon Sep 17 00:00:00 2001 From: Bjarne Koll Date: Sun, 26 Jan 2025 20:59:32 +0100 Subject: [PATCH 2/5] Validate first --- .../java/io/papermc/paper/entity/PlayerGiveResult.java | 3 +++ .../org/bukkit/craftbukkit/entity/CraftPlayer.java | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/paper-api/src/main/java/io/papermc/paper/entity/PlayerGiveResult.java b/paper-api/src/main/java/io/papermc/paper/entity/PlayerGiveResult.java index 9972f3edbdcf..cf6a9706e9ac 100644 --- a/paper-api/src/main/java/io/papermc/paper/entity/PlayerGiveResult.java +++ b/paper-api/src/main/java/io/papermc/paper/entity/PlayerGiveResult.java @@ -16,6 +16,9 @@ public interface PlayerGiveResult { * A collection of itemstacks that were not added to the player's inventory as they did not fit. * The collection is derived from the collections of items to add by creating copies of each stack that was not * fully added to the inventory and assigning the non-added count as their amount. + *

+ * Itemstacks found here *may* also be found as item entities in the {@link #drops()} collection, as the + * give logic may have dropped them. * * @return the unmodifiable collection of itemstacks that are leftover as they could not be added. Each element is a * copy of the input stack they are derived from. diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index 30c33891e5a7..57a4041b625d 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -3557,13 +3557,17 @@ public void sendEntityEffect(final org.bukkit.EntityEffect effect, final org.buk Preconditions.checkNotNull(items, "items cannot be null"); if (items.isEmpty()) return PaperPlayerGiveResult.EMPTY; // Early opt out for empty input. - final ServerPlayer handle = this.getHandle(); - final ImmutableList.Builder drops = ImmutableList.builder(); - final ImmutableList.Builder leftovers = ImmutableList.builder(); + // Validate all items before attempting to spawn any. for (final ItemStack item : items) { Preconditions.checkNotNull(item, "ItemStack cannot be null"); Preconditions.checkArgument(!item.isEmpty(), "ItemStack cannot be empty"); Preconditions.checkArgument(item.getAmount() <= item.getMaxStackSize(), "ItemStack amount cannot be greater than its max stack size"); + } + + final ServerPlayer handle = this.getHandle(); + final ImmutableList.Builder drops = ImmutableList.builder(); + final ImmutableList.Builder leftovers = ImmutableList.builder(); + for (final ItemStack item : items) { final net.minecraft.world.item.ItemStack nmsStack = CraftItemStack.asNMSCopy(item); final boolean added = handle.getInventory().add(nmsStack); if (added && nmsStack.isEmpty()) continue; // Item was fully added, neither a drop nor a leftover is needed. From f2038af3f919eb71bc4161fce29c445f776f20b1 Mon Sep 17 00:00:00 2001 From: Bjarne Koll Date: Sun, 26 Jan 2025 21:02:25 +0100 Subject: [PATCH 3/5] Drop useless import --- .../src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index 57a4041b625d..9860f7d9672f 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -24,7 +24,6 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; From fbca348dd712467fc02fa9260a292425315815b7 Mon Sep 17 00:00:00 2001 From: Bjarne Koll Date: Sun, 26 Jan 2025 21:09:23 +0100 Subject: [PATCH 4/5] Switch to checkArgument --- .../main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index 9860f7d9672f..26f749e0bd91 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -3558,7 +3558,7 @@ public void sendEntityEffect(final org.bukkit.EntityEffect effect, final org.buk // Validate all items before attempting to spawn any. for (final ItemStack item : items) { - Preconditions.checkNotNull(item, "ItemStack cannot be null"); + Preconditions.checkArgument(item != null, "ItemStack cannot be null"); Preconditions.checkArgument(!item.isEmpty(), "ItemStack cannot be empty"); Preconditions.checkArgument(item.getAmount() <= item.getMaxStackSize(), "ItemStack amount cannot be greater than its max stack size"); } From bdc9c50437e56eb921fe17ecc0e9c2172044ecfa Mon Sep 17 00:00:00 2001 From: Bjarne Koll Date: Sun, 26 Jan 2025 21:12:42 +0100 Subject: [PATCH 5/5] Part 2 --- .../main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index 26f749e0bd91..baffa036078b 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -3553,7 +3553,7 @@ public void sendEntityEffect(final org.bukkit.EntityEffect effect, final org.buk @Override public @NotNull PlayerGiveResult give(@NotNull final Collection<@NotNull ItemStack> items, final boolean dropIfFull) { - Preconditions.checkNotNull(items, "items cannot be null"); + Preconditions.checkArgument(items != null, "items cannot be null"); if (items.isEmpty()) return PaperPlayerGiveResult.EMPTY; // Early opt out for empty input. // Validate all items before attempting to spawn any.