--- a/net/minecraft/world/entity/item/ItemEntity.java
+++ b/net/minecraft/world/entity/item/ItemEntity.java
@@ -53,6 +_,9 @@
     @Nullable
     public UUID target;
     public final float bobOffs;
+    public boolean canMobPickup = true; // Paper - Item#canEntityPickup
+    private int despawnRate = -1; // Paper - Alternative item-despawn-rate
+    public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper - Friction API
 
     public ItemEntity(EntityType<? extends ItemEntity> entityType, Level level) {
         super(entityType, level);
@@ -61,7 +_,12 @@
     }
 
     public ItemEntity(Level level, double posX, double posY, double posZ, ItemStack itemStack) {
-        this(level, posX, posY, posZ, itemStack, level.random.nextDouble() * 0.2 - 0.1, 0.2, level.random.nextDouble() * 0.2 - 0.1);
+        // Paper start - Don't use level random in entity constructors (to make them thread-safe)
+        this(EntityType.ITEM, level);
+        this.setPos(posX, posY, posZ);
+        this.setDeltaMovement(this.random.nextDouble() * 0.2 - 0.1, 0.2, this.random.nextDouble() * 0.2 - 0.1);
+        this.setItem(itemStack);
+        // Paper end - Don't use level random in entity constructors
     }
 
     public ItemEntity(Level level, double posX, double posY, double posZ, ItemStack itemStack, double deltaX, double deltaY, double deltaZ) {
@@ -79,6 +_,14 @@
         this.bobOffs = other.bobOffs;
     }
 
+    // Paper start - Require item entities to send their location precisely (Fixes MC-4)
+    {
+        if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.sendFullPosForItemEntities) {
+            this.setRequiresPrecisePosition(true);
+        }
+    }
+    // Paper end - Require item entities to send their location precisely (Fixes MC-4)
+
     @Override
     public boolean dampensVibrations() {
         return this.getItem().is(ItemTags.DAMPENS_VIBRATIONS);
@@ -116,7 +_,7 @@
     @Override
     public void tick() {
         if (this.getItem().isEmpty()) {
-            this.discard();
+            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
         } else {
             super.tick();
             if (this.pickupDelay > 0 && this.pickupDelay != 32767) {
@@ -144,11 +_,15 @@
                 }
             }
 
-            if (!this.onGround() || this.getDeltaMovement().horizontalDistanceSqr() > 1.0E-5F || (this.tickCount + this.getId()) % 4 == 0) {
+            if (!this.onGround() || this.getDeltaMovement().horizontalDistanceSqr() > 1.0E-5F || (this.tickCount + this.getId()) % 4 == 0) { // Paper - Diff on change; ActivationRange immunity
                 this.move(MoverType.SELF, this.getDeltaMovement());
                 this.applyEffectsFromBlocks();
                 float f = 0.98F;
-                if (this.onGround()) {
+                // Paper start - Friction API
+                if (this.frictionState == net.kyori.adventure.util.TriState.FALSE) {
+                    f = 1F;
+                } else if (this.onGround()) {
+                    // Paper end - Friction API
                     f = this.level().getBlockState(this.getBlockPosBelowThatAffectsMyMovement()).getBlock().getFriction() * 0.98F;
                 }
 
@@ -181,8 +_,14 @@
                 }
             }
 
-            if (!this.level().isClientSide && this.age >= 6000) {
-                this.discard();
+            if (!this.level().isClientSide && this.age >= this.despawnRate) { // Spigot // Paper - Alternative item-despawn-rate
+                // CraftBukkit start - fire ItemDespawnEvent
+                if (org.bukkit.craftbukkit.event.CraftEventFactory.callItemDespawnEvent(this).isCancelled()) {
+                    this.age = 0;
+                    return;
+                }
+                // CraftBukkit end
+                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
             }
         }
     }
@@ -207,9 +_,18 @@
 
     private void mergeWithNeighbours() {
         if (this.isMergable()) {
+            double radius = this.level().spigotConfig.itemMerge; // Spigot
             for (ItemEntity itemEntity : this.level()
-                .getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(0.5, 0.0, 0.5), neighbour -> neighbour != this && neighbour.isMergable())) {
+                .getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(radius, this.level().paperConfig().entities.behavior.onlyMergeItemsHorizontally ? 0 : radius - 0.5D, radius), neighbour -> neighbour != this && neighbour.isMergable())) { // Spigot // Paper - configuration to only merge items horizontally
                 if (itemEntity.isMergable()) {
+                    // Paper start - Fix items merging through walls
+                    if (this.level().paperConfig().fixes.fixItemsMergingThroughWalls) {
+                        if (this.level().clipDirect(this.position(), itemEntity.position(),
+                            net.minecraft.world.phys.shapes.CollisionContext.of(this)) == net.minecraft.world.phys.HitResult.Type.BLOCK) {
+                            continue;
+                        }
+                    }
+                    // Paper end - Fix items merging through walls
                     this.tryToMerge(itemEntity);
                     if (this.isRemoved()) {
                         break;
@@ -221,7 +_,7 @@
 
     private boolean isMergable() {
         ItemStack item = this.getItem();
-        return this.isAlive() && this.pickupDelay != 32767 && this.age != -32768 && this.age < 6000 && item.getCount() < item.getMaxStackSize();
+        return this.isAlive() && this.pickupDelay != 32767 && this.age != -32768 && this.age < this.despawnRate && item.getCount() < item.getMaxStackSize(); // Paper - Alternative item-despawn-rate
     }
 
     private void tryToMerge(ItemEntity itemEntity) {
@@ -254,11 +_,16 @@
     }
 
     private static void merge(ItemEntity destinationEntity, ItemStack destinationStack, ItemEntity originEntity, ItemStack originStack) {
+        // CraftBukkit start
+        if (!org.bukkit.craftbukkit.event.CraftEventFactory.callItemMergeEvent(originEntity, destinationEntity)) {
+            return;
+        }
+        // CraftBukkit end
         merge(destinationEntity, destinationStack, originStack);
         destinationEntity.pickupDelay = Math.max(destinationEntity.pickupDelay, originEntity.pickupDelay);
         destinationEntity.age = Math.min(destinationEntity.age, originEntity.age);
         if (originStack.isEmpty()) {
-            originEntity.discard();
+            originEntity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.MERGE); // CraftBukkit - add Bukkit remove cause
         }
     }
 
@@ -286,12 +_,17 @@
         } else if (!this.getItem().canBeHurtBy(damageSource)) {
             return false;
         } else {
+            // CraftBukkit start
+            if (org.bukkit.craftbukkit.event.CraftEventFactory.handleNonLivingEntityDamageEvent(this, damageSource, amount)) {
+                return false;
+            }
+            // CraftBukkit end
             this.markHurt();
             this.health = (int)(this.health - amount);
             this.gameEvent(GameEvent.ENTITY_DAMAGE, damageSource.getEntity());
             if (this.health <= 0) {
                 this.getItem().onDestroyed(this);
-                this.discard();
+                this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
             }
 
             return true;
@@ -313,6 +_,11 @@
         if (!this.getItem().isEmpty()) {
             output.store("Item", ItemStack.CODEC, this.getItem());
         }
+        // Paper start - Friction API
+        if (this.frictionState != net.kyori.adventure.util.TriState.NOT_SET) {
+            output.putString("Paper.FrictionState", this.frictionState.toString());
+        }
+        // Paper end - Friction API
     }
 
     @Override
@@ -323,8 +_,17 @@
         this.target = input.read("Owner", UUIDUtil.CODEC).orElse(null);
         this.thrower = EntityReference.read(input, "Thrower");
         this.setItem(input.read("Item", ItemStack.CODEC).orElse(ItemStack.EMPTY));
+        // Paper start - Friction API
+        input.getString("Paper.FrictionState").ifPresent(frictionState -> {
+            try {
+                this.frictionState = net.kyori.adventure.util.TriState.valueOf(frictionState);
+            } catch (Exception ignored) {
+                com.mojang.logging.LogUtils.getLogger().error("Unknown friction state {} for {}", frictionState, this);
+            }
+        });
+        // Paper end - Friction API
         if (this.getItem().isEmpty()) {
-            this.discard();
+            this.discard(null); // CraftBukkit - add Bukkit remove cause
         }
     }
 
@@ -334,10 +_,73 @@
             ItemStack item = this.getItem();
             Item item1 = item.getItem();
             int count = item.getCount();
+            // CraftBukkit start - fire PlayerPickupItemEvent
+            int canHold = entity.getInventory().canHold(item);
+            int remaining = count - canHold;
+            boolean flyAtPlayer = false; // Paper
+
+            // Paper start - PlayerAttemptPickupItemEvent
+            if (this.pickupDelay <= 0) {
+                org.bukkit.event.player.PlayerAttemptPickupItemEvent attemptEvent = new org.bukkit.event.player.PlayerAttemptPickupItemEvent((org.bukkit.entity.Player) entity.getBukkitEntity(), (org.bukkit.entity.Item) this.getBukkitEntity(), remaining);
+                this.level().getCraftServer().getPluginManager().callEvent(attemptEvent);
+
+                flyAtPlayer = attemptEvent.getFlyAtPlayer();
+                if (attemptEvent.isCancelled()) {
+                    if (flyAtPlayer) {
+                        entity.take(this, count);
+                    }
+
+                    return;
+                }
+            }
+
+            if (this.pickupDelay <= 0 && canHold > 0) {
+                item.setCount(canHold);
+                // Call legacy event
+                org.bukkit.event.player.PlayerPickupItemEvent playerEvent = new org.bukkit.event.player.PlayerPickupItemEvent((org.bukkit.entity.Player) entity.getBukkitEntity(), (org.bukkit.entity.Item) this.getBukkitEntity(), remaining);
+                playerEvent.setCancelled(!playerEvent.getPlayer().getCanPickupItems());
+                this.level().getCraftServer().getPluginManager().callEvent(playerEvent);
+                flyAtPlayer = playerEvent.getFlyAtPlayer(); // Paper
+                if (playerEvent.isCancelled()) {
+                    item.setCount(count); // SPIGOT-5294 - restore count
+                    // Paper start
+                    if (flyAtPlayer) {
+                        entity.take(this, count);
+                    }
+                    // Paper end
+                    return;
+                }
+
+                // Call newer event afterwards
+                org.bukkit.event.entity.EntityPickupItemEvent entityEvent = new org.bukkit.event.entity.EntityPickupItemEvent(entity.getBukkitEntity(), (org.bukkit.entity.Item) this.getBukkitEntity(), remaining);
+                entityEvent.setCancelled(!entityEvent.getEntity().getCanPickupItems());
+                this.level().getCraftServer().getPluginManager().callEvent(entityEvent);
+                if (entityEvent.isCancelled()) {
+                    item.setCount(count); // SPIGOT-5294 - restore count
+                    return;
+                }
+
+                // Update the ItemStack if it was changed in the event
+                ItemStack current = this.getItem();
+                if (!item.equals(current)) {
+                    item = current;
+                } else {
+                    item.setCount(canHold + remaining); // = i
+                }
+
+                // Possibly < 0; fix here so we do not have to modify code below
+                this.pickupDelay = 0;
+            } else if (this.pickupDelay == 0) {
+                // ensure that the code below isn't triggered if canHold says we can't pick the items up
+                this.pickupDelay = -1;
+            }
+            // CraftBukkit end
+            // Paper end - PlayerAttemptPickupItemEvent
             if (this.pickupDelay == 0 && (this.target == null || this.target.equals(entity.getUUID())) && entity.getInventory().add(item)) {
+                if (flyAtPlayer) // Paper - PlayerPickupItemEvent
                 entity.take(this, count);
                 if (item.isEmpty()) {
-                    this.discard();
+                    this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
                     item.setCount(count);
                 }
 
@@ -375,6 +_,7 @@
 
     public void setItem(ItemStack stack) {
         this.getEntityData().set(DATA_ITEM, stack);
+        this.despawnRate = this.level().paperConfig().entities.spawning.altItemDespawnRate.enabled ? this.level().paperConfig().entities.spawning.altItemDespawnRate.items.getOrDefault(stack.getItem(), this.level().spigotConfig.itemDespawnRate) : this.level().spigotConfig.itemDespawnRate; // Paper - Alternative item-despawn-rate
     }
 
     @Override
@@ -427,7 +_,7 @@
 
     public void makeFakeItem() {
         this.setNeverPickUp();
-        this.age = 5999;
+        this.age = this.despawnRate - 1; // Spigot // Paper - Alternative item-despawn-rate
     }
 
     public static float getSpin(float age, float bobOffset) {
