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..cf6a9706e9ac
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/entity/PlayerGiveResult.java
@@ -0,0 +1,39 @@
+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.
+ *
+ * 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.
+ */
+ @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..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
@@ -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;
@@ -101,6 +104,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 +180,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 +3550,35 @@ 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.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.
+ for (final ItemStack item : items) {
+ 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");
+ }
+
+ 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.
+
+ 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());
+ }
}