--- a/net/minecraft/world/entity/ExperienceOrb.java
+++ b/net/minecraft/world/entity/ExperienceOrb.java
@@ -1,5 +1,6 @@
 package net.minecraft.world.entity;
 
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map.Entry;
 import net.minecraft.core.BlockPos;
@@ -20,31 +21,93 @@
 import net.minecraft.world.level.entity.EntityTypeTest;
 import net.minecraft.world.phys.AABB;
 import net.minecraft.world.phys.Vec3;
+// CraftBukkit start
+import org.bukkit.craftbukkit.event.CraftEventFactory;
+import org.bukkit.event.entity.EntityRemoveEvent;
+import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
+import org.bukkit.event.entity.EntityTargetEvent;
+import org.bukkit.event.player.PlayerExpCooldownChangeEvent;
+// CraftBukkit end
 
 public class ExperienceOrb extends Entity {
+
     private static final int LIFETIME = 6000;
     private static final int ENTITY_SCAN_PERIOD = 20;
     private static final int MAX_FOLLOW_DIST = 8;
     private static final int ORB_GROUPS_PER_AREA = 40;
-    private static final double ORB_MERGE_DISTANCE = 0.5;
+    private static final double ORB_MERGE_DISTANCE = 0.5D;
     private int age;
-    private int health = 5;
+    private int health;
     public int value;
-    public int count = 1;
+    public int count;
     private Player followingPlayer;
+    // Paper start
+    @javax.annotation.Nullable
+    public java.util.UUID sourceEntityId;
+    @javax.annotation.Nullable
+    public java.util.UUID triggerEntityId;
+    public org.bukkit.entity.ExperienceOrb.SpawnReason spawnReason = org.bukkit.entity.ExperienceOrb.SpawnReason.UNKNOWN;
+
+    private void loadPaperNBT(CompoundTag tag) {
+        if (!tag.contains("Paper.ExpData", net.minecraft.nbt.Tag.TAG_COMPOUND)) {
+            return;
+        }
+        CompoundTag comp = tag.getCompound("Paper.ExpData");
+        if (comp.hasUUID("source")) {
+            this.sourceEntityId = comp.getUUID("source");
+        }
+        if (comp.hasUUID("trigger")) {
+            this.triggerEntityId = comp.getUUID("trigger");
+        }
+        if (comp.contains("reason")) {
+            String reason = comp.getString("reason");
+            try {
+                this.spawnReason = org.bukkit.entity.ExperienceOrb.SpawnReason.valueOf(reason);
+            } catch (Exception e) {
+                this.level().getCraftServer().getLogger().warning("Invalid spawnReason set for experience orb: " + e.getMessage() + " - " + reason);
+            }
+        }
+    }
+    private void savePaperNBT(CompoundTag tag) {
+        CompoundTag comp = new CompoundTag();
+        if (this.sourceEntityId != null) {
+            comp.putUUID("source", this.sourceEntityId);
+        }
+        if (this.triggerEntityId != null) {
+            comp.putUUID("trigger", triggerEntityId);
+        }
+        if (this.spawnReason != null && this.spawnReason != org.bukkit.entity.ExperienceOrb.SpawnReason.UNKNOWN) {
+            comp.putString("reason", this.spawnReason.name());
+        }
+        tag.put("Paper.ExpData", comp);
+    }
 
+    @io.papermc.paper.annotation.DoNotUse
+    @Deprecated
     public ExperienceOrb(Level world, double x, double y, double z, int amount) {
+        this(world, x, y, z, amount, null, null);
+    }
+
+    public ExperienceOrb(Level world, double x, double y, double z, int amount, @javax.annotation.Nullable org.bukkit.entity.ExperienceOrb.SpawnReason reason, @javax.annotation.Nullable Entity triggerId) {
+        this(world, x, y, z, amount, reason, triggerId, null);
+    }
+
+    public ExperienceOrb(Level world, double x, double y, double z, int amount, @javax.annotation.Nullable org.bukkit.entity.ExperienceOrb.SpawnReason reason, @javax.annotation.Nullable Entity triggerId, @javax.annotation.Nullable Entity sourceId) {
         this(EntityType.EXPERIENCE_ORB, world);
+        this.sourceEntityId = sourceId != null ? sourceId.getUUID() : null;
+        this.triggerEntityId = triggerId != null ? triggerId.getUUID() : null;
+        this.spawnReason = reason != null ? reason : org.bukkit.entity.ExperienceOrb.SpawnReason.UNKNOWN;
+        // Paper end
         this.setPos(x, y, z);
-        this.setYRot((float)(this.random.nextDouble() * 360.0));
-        this.setDeltaMovement(
-            (this.random.nextDouble() * 0.2F - 0.1F) * 2.0, this.random.nextDouble() * 0.2 * 2.0, (this.random.nextDouble() * 0.2F - 0.1F) * 2.0
-        );
+        this.setYRot((float) (this.random.nextDouble() * 360.0D));
+        this.setDeltaMovement((this.random.nextDouble() * 0.20000000298023224D - 0.10000000149011612D) * 2.0D, this.random.nextDouble() * 0.2D * 2.0D, (this.random.nextDouble() * 0.20000000298023224D - 0.10000000149011612D) * 2.0D);
         this.value = amount;
     }
 
     public ExperienceOrb(EntityType<? extends ExperienceOrb> type, Level world) {
         super(type, world);
+        this.health = 5;
+        this.count = 1;
     }
 
     @Override
@@ -53,17 +116,17 @@
     }
 
     @Override
-    protected void defineSynchedData(SynchedEntityData.Builder builder) {
-    }
+    protected void defineSynchedData(SynchedEntityData.Builder builder) {}
 
     @Override
     protected double getDefaultGravity() {
-        return 0.03;
+        return 0.03D;
     }
 
     @Override
     public void tick() {
         super.tick();
+        Player prevTarget = this.followingPlayer;// CraftBukkit - store old target
         this.xo = this.getX();
         this.yo = this.getY();
         this.zo = this.getZ();
@@ -74,15 +137,11 @@
         }
 
         if (this.level().getFluidState(this.blockPosition()).is(FluidTags.LAVA)) {
-            this.setDeltaMovement(
-                (double)((this.random.nextFloat() - this.random.nextFloat()) * 0.2F),
-                0.2F,
-                (double)((this.random.nextFloat() - this.random.nextFloat()) * 0.2F)
-            );
+            this.setDeltaMovement((double) ((this.random.nextFloat() - this.random.nextFloat()) * 0.2F), 0.20000000298023224D, (double) ((this.random.nextFloat() - this.random.nextFloat()) * 0.2F));
         }
 
         if (!this.level().noCollision(this.getBoundingBox())) {
-            this.moveTowardsClosestSpace(this.getX(), (this.getBoundingBox().minY + this.getBoundingBox().maxY) / 2.0, this.getZ());
+            this.moveTowardsClosestSpace(this.getX(), (this.getBoundingBox().minY + this.getBoundingBox().maxY) / 2.0D, this.getZ());
         }
 
         if (this.tickCount % 20 == 1) {
@@ -93,34 +152,49 @@
             this.followingPlayer = null;
         }
 
-        if (this.followingPlayer != null) {
-            Vec3 vec3 = new Vec3(
-                this.followingPlayer.getX() - this.getX(),
-                this.followingPlayer.getY() + (double)this.followingPlayer.getEyeHeight() / 2.0 - this.getY(),
-                this.followingPlayer.getZ() - this.getZ()
-            );
-            double d = vec3.lengthSqr();
-            if (d < 64.0) {
-                double e = 1.0 - Math.sqrt(d) / 8.0;
-                this.setDeltaMovement(this.getDeltaMovement().add(vec3.normalize().scale(e * e * 0.1)));
+        // CraftBukkit start
+        boolean cancelled = false;
+        if (this.followingPlayer != prevTarget) {
+            EntityTargetLivingEntityEvent event = CraftEventFactory.callEntityTargetLivingEvent(this, this.followingPlayer, (this.followingPlayer != null) ? EntityTargetEvent.TargetReason.CLOSEST_PLAYER : EntityTargetEvent.TargetReason.FORGOT_TARGET);
+            LivingEntity target = (event.getTarget() == null) ? null : ((org.bukkit.craftbukkit.entity.CraftLivingEntity) event.getTarget()).getHandle();
+            cancelled = event.isCancelled();
+
+            if (cancelled) {
+                this.followingPlayer = prevTarget;
+            } else {
+                this.followingPlayer = (target instanceof Player) ? (Player) target : null;
+            }
+        }
+
+        if (this.followingPlayer != null && !cancelled) {
+            // CraftBukkit end
+            Vec3 vec3d = new Vec3(this.followingPlayer.getX() - this.getX(), this.followingPlayer.getY() + (double) this.followingPlayer.getEyeHeight() / 2.0D - this.getY(), this.followingPlayer.getZ() - this.getZ());
+            double d0 = vec3d.lengthSqr();
+
+            if (d0 < 64.0D) {
+                double d1 = 1.0D - Math.sqrt(d0) / 8.0D;
+
+                this.setDeltaMovement(this.getDeltaMovement().add(vec3d.normalize().scale(d1 * d1 * 0.1D)));
             }
         }
 
         this.move(MoverType.SELF, this.getDeltaMovement());
         float f = 0.98F;
+
         if (this.onGround()) {
             f = this.level().getBlockState(this.getBlockPosBelowThatAffectsMyMovement()).getBlock().getFriction() * 0.98F;
         }
 
-        this.setDeltaMovement(this.getDeltaMovement().multiply((double)f, 0.98, (double)f));
+        this.setDeltaMovement(this.getDeltaMovement().multiply((double) f, 0.98D, (double) f));
         if (this.onGround()) {
-            this.setDeltaMovement(this.getDeltaMovement().multiply(1.0, -0.9, 1.0));
+            this.setDeltaMovement(this.getDeltaMovement().multiply(1.0D, -0.9D, 1.0D));
         }
 
-        this.age++;
+        ++this.age;
         if (this.age >= 6000) {
-            this.discard();
+            this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
         }
+
     }
 
     @Override
@@ -129,36 +203,56 @@
     }
 
     private void scanForEntities() {
-        if (this.followingPlayer == null || this.followingPlayer.distanceToSqr(this) > 64.0) {
-            this.followingPlayer = this.level().getNearestPlayer(this, 8.0);
+        if (this.followingPlayer == null || this.followingPlayer.distanceToSqr((Entity) this) > 64.0D) {
+            this.followingPlayer = this.level().getNearestPlayer(this, 8.0D);
         }
 
         if (this.level() instanceof ServerLevel) {
-            for (ExperienceOrb experienceOrb : this.level()
-                .getEntities(EntityTypeTest.forClass(ExperienceOrb.class), this.getBoundingBox().inflate(0.5), this::canMerge)) {
-                this.merge(experienceOrb);
+            List<ExperienceOrb> list = this.level().getEntities(EntityTypeTest.forClass(ExperienceOrb.class), this.getBoundingBox().inflate(0.5D), this::canMerge);
+            Iterator iterator = list.iterator();
+
+            while (iterator.hasNext()) {
+                ExperienceOrb entityexperienceorb = (ExperienceOrb) iterator.next();
+
+                this.merge(entityexperienceorb);
             }
         }
+
     }
 
     public static void award(ServerLevel world, Vec3 pos, int amount) {
+        // Paper start - add reasons for orbs
+        award(world, pos, amount, null, null, null);
+    }
+    public static void award(ServerLevel world, Vec3 pos, int amount, org.bukkit.entity.ExperienceOrb.SpawnReason reason, Entity triggerId) {
+        award(world, pos, amount, reason, triggerId, null);
+    }
+    public static void award(ServerLevel world, Vec3 pos, int amount, org.bukkit.entity.ExperienceOrb.SpawnReason reason, Entity triggerId, Entity sourceId) {
+        // Paper end - add reasons for orbs
         while (amount > 0) {
-            int i = getExperienceValue(amount);
-            amount -= i;
-            if (!tryMergeToExisting(world, pos, i)) {
-                world.addFreshEntity(new ExperienceOrb(world, pos.x(), pos.y(), pos.z(), i));
+            int j = ExperienceOrb.getExperienceValue(amount);
+
+            amount -= j;
+            if (!ExperienceOrb.tryMergeToExisting(world, pos, j)) {
+                world.addFreshEntity(new ExperienceOrb(world, pos.x(), pos.y(), pos.z(), j, reason, triggerId, sourceId)); // Paper - add reason
             }
         }
+
     }
 
     private static boolean tryMergeToExisting(ServerLevel world, Vec3 pos, int amount) {
-        AABB aABB = AABB.ofSize(pos, 1.0, 1.0, 1.0);
-        int i = world.getRandom().nextInt(40);
-        List<ExperienceOrb> list = world.getEntities(EntityTypeTest.forClass(ExperienceOrb.class), aABB, orb -> canMerge(orb, i, amount));
+        // Paper - TODO some other event for this kind of merge
+        AABB axisalignedbb = AABB.ofSize(pos, 1.0D, 1.0D, 1.0D);
+        int j = world.getRandom().nextInt(40);
+        List<ExperienceOrb> list = world.getEntities(EntityTypeTest.forClass(ExperienceOrb.class), axisalignedbb, (entityexperienceorb) -> {
+            return ExperienceOrb.canMerge(entityexperienceorb, j, amount);
+        });
+
         if (!list.isEmpty()) {
-            ExperienceOrb experienceOrb = list.get(0);
-            experienceOrb.count++;
-            experienceOrb.age = 0;
+            ExperienceOrb entityexperienceorb = (ExperienceOrb) list.get(0);
+
+            ++entityexperienceorb.count;
+            entityexperienceorb.age = 0;
             return true;
         } else {
             return false;
@@ -166,7 +260,7 @@
     }
 
     private boolean canMerge(ExperienceOrb other) {
-        return other != this && canMerge(other, this.getId(), this.value);
+        return other != this && ExperienceOrb.canMerge(other, this.getId(), this.value);
     }
 
     private static boolean canMerge(ExperienceOrb orb, int seed, int amount) {
@@ -174,19 +268,24 @@
     }
 
     private void merge(ExperienceOrb other) {
-        this.count = this.count + other.count;
+        // Paper start - call orb merge event
+        if (!new com.destroystokyo.paper.event.entity.ExperienceOrbMergeEvent((org.bukkit.entity.ExperienceOrb) this.getBukkitEntity(), (org.bukkit.entity.ExperienceOrb) other.getBukkitEntity()).callEvent()) {
+            return;
+        }
+        // Paper end - call orb merge event
+        this.count += other.count;
         this.age = Math.min(this.age, other.age);
-        other.discard();
+        other.discard(EntityRemoveEvent.Cause.MERGE); // CraftBukkit - add Bukkit remove cause
     }
 
     private void setUnderwaterMovement() {
-        Vec3 vec3 = this.getDeltaMovement();
-        this.setDeltaMovement(vec3.x * 0.99F, Math.min(vec3.y + 5.0E-4F, 0.06F), vec3.z * 0.99F);
+        Vec3 vec3d = this.getDeltaMovement();
+
+        this.setDeltaMovement(vec3d.x * 0.9900000095367432D, Math.min(vec3d.y + 5.000000237487257E-4D, 0.05999999865889549D), vec3d.z * 0.9900000095367432D);
     }
 
     @Override
-    protected void doWaterSplashEffect() {
-    }
+    protected void doWaterSplashEffect() {}
 
     @Override
     public boolean hurt(DamageSource source, float amount) {
@@ -196,9 +295,9 @@
             return true;
         } else {
             this.markHurt();
-            this.health = (int)((float)this.health - amount);
+            this.health = (int) ((float) this.health - amount);
             if (this.health <= 0) {
-                this.discard();
+                this.discard(EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
             }
 
             return true;
@@ -207,47 +306,67 @@
 
     @Override
     public void addAdditionalSaveData(CompoundTag nbt) {
-        nbt.putShort("Health", (short)this.health);
-        nbt.putShort("Age", (short)this.age);
-        nbt.putShort("Value", (short)this.value);
+        nbt.putShort("Health", (short) this.health);
+        nbt.putShort("Age", (short) this.age);
+        nbt.putInt("Value", this.value); // Paper - save as Integer
         nbt.putInt("Count", this.count);
+        this.savePaperNBT(nbt); // Paper
     }
 
     @Override
     public void readAdditionalSaveData(CompoundTag nbt) {
         this.health = nbt.getShort("Health");
         this.age = nbt.getShort("Age");
-        this.value = nbt.getShort("Value");
+        this.value = nbt.getInt("Value"); // Paper - load as Integer
         this.count = Math.max(nbt.getInt("Count"), 1);
+        this.loadPaperNBT(nbt); // Paper
     }
 
     @Override
     public void playerTouch(Player player) {
         if (!this.level().isClientSide) {
-            if (player.takeXpDelay == 0) {
-                player.takeXpDelay = 2;
+            if (player.takeXpDelay == 0 && new com.destroystokyo.paper.event.player.PlayerPickupExperienceEvent(((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity(), (org.bukkit.entity.ExperienceOrb) this.getBukkitEntity()).callEvent()) { // Paper - PlayerPickupExperienceEvent
+                player.takeXpDelay = CraftEventFactory.callPlayerXpCooldownEvent(player, 2, PlayerExpCooldownChangeEvent.ChangeReason.PICKUP_ORB).getNewCooldown(); // CraftBukkit - entityhuman.takeXpDelay = 2;
                 player.take(this, 1);
                 int i = this.repairPlayerItems(player, this.value);
+
                 if (i > 0) {
-                    player.giveExperiencePoints(i);
+                    player.giveExperiencePoints(CraftEventFactory.callPlayerExpChangeEvent(player, this).getAmount()); // CraftBukkit - this.value -> event.getAmount() // Paper - supply experience orb object
                 }
 
-                this.count--;
+                --this.count;
                 if (this.count == 0) {
-                    this.discard();
+                    this.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
                 }
             }
+
         }
     }
 
     private int repairPlayerItems(Player player, int amount) {
         Entry<EquipmentSlot, ItemStack> entry = EnchantmentHelper.getRandomItemWith(Enchantments.MENDING, player, ItemStack::isDamaged);
+
         if (entry != null) {
-            ItemStack itemStack = entry.getValue();
-            int i = Math.min(this.xpToDurability(amount), itemStack.getDamageValue());
-            itemStack.setDamageValue(itemStack.getDamageValue() - i);
-            int j = amount - this.durabilityToXp(i);
-            return j > 0 ? this.repairPlayerItems(player, j) : 0;
+            ItemStack itemstack = (ItemStack) entry.getValue();
+            int j = Math.min(this.xpToDurability(amount), itemstack.getDamageValue());
+            // CraftBukkit start
+            org.bukkit.event.player.PlayerItemMendEvent event = CraftEventFactory.callPlayerItemMendEvent(player, this, itemstack, entry.getKey(), j, this::durabilityToXp); // Paper - Expand PlayerItemMendEvent
+            j = event.getRepairAmount();
+            if (event.isCancelled()) {
+                return amount;
+            }
+            // CraftBukkit end
+
+            itemstack.setDamageValue(itemstack.getDamageValue() - j);
+            int k = amount - event.getDurabilityToXpOperation().applyAsInt(j); // Paper - Expand PlayerItemMendEvent
+            // this.value = k; // CraftBukkit - update exp value of orb for PlayerItemMendEvent calls // Paper - the value field should not be mutated here because it doesn't take "count" into account
+            // Paper start - Expand PlayerItemMendEvent
+            if (j == 0 && amount == k) { // if repair amount is 0 and no xp was removed, don't do recursion; treat as cancelled
+                return k;
+            }
+            // Paper end - Expand PlayerItemMendEvent
+
+            return k > 0 ? this.repairPlayerItems(player, k) : 0;
         } else {
             return amount;
         }
@@ -266,51 +385,29 @@
     }
 
     public int getIcon() {
-        if (this.value >= 2477) {
-            return 10;
-        } else if (this.value >= 1237) {
-            return 9;
-        } else if (this.value >= 617) {
-            return 8;
-        } else if (this.value >= 307) {
-            return 7;
-        } else if (this.value >= 149) {
-            return 6;
-        } else if (this.value >= 73) {
-            return 5;
-        } else if (this.value >= 37) {
-            return 4;
-        } else if (this.value >= 17) {
-            return 3;
-        } else if (this.value >= 7) {
-            return 2;
-        } else {
-            return this.value >= 3 ? 1 : 0;
-        }
+        return this.value >= 2477 ? 10 : (this.value >= 1237 ? 9 : (this.value >= 617 ? 8 : (this.value >= 307 ? 7 : (this.value >= 149 ? 6 : (this.value >= 73 ? 5 : (this.value >= 37 ? 4 : (this.value >= 17 ? 3 : (this.value >= 7 ? 2 : (this.value >= 3 ? 1 : 0)))))))));
     }
 
     public static int getExperienceValue(int value) {
-        if (value >= 2477) {
-            return 2477;
-        } else if (value >= 1237) {
-            return 1237;
-        } else if (value >= 617) {
-            return 617;
-        } else if (value >= 307) {
-            return 307;
-        } else if (value >= 149) {
-            return 149;
-        } else if (value >= 73) {
-            return 73;
-        } else if (value >= 37) {
-            return 37;
-        } else if (value >= 17) {
-            return 17;
-        } else if (value >= 7) {
-            return 7;
-        } else {
-            return value >= 3 ? 3 : 1;
-        }
+        // CraftBukkit start
+        if (value > 162670129) return value - 100000;
+        if (value > 81335063) return 81335063;
+        if (value > 40667527) return 40667527;
+        if (value > 20333759) return 20333759;
+        if (value > 10166857) return 10166857;
+        if (value > 5083423) return 5083423;
+        if (value > 2541701) return 2541701;
+        if (value > 1270849) return 1270849;
+        if (value > 635413) return 635413;
+        if (value > 317701) return 317701;
+        if (value > 158849) return 158849;
+        if (value > 79423) return 79423;
+        if (value > 39709) return 39709;
+        if (value > 19853) return 19853;
+        if (value > 9923) return 9923;
+        if (value > 4957) return 4957;
+        // CraftBukkit end
+        return value >= 2477 ? 2477 : (value >= 1237 ? 1237 : (value >= 617 ? 617 : (value >= 307 ? 307 : (value >= 149 ? 149 : (value >= 73 ? 73 : (value >= 37 ? 37 : (value >= 17 ? 17 : (value >= 7 ? 7 : (value >= 3 ? 3 : 1)))))))));
     }
 
     @Override
