--- a/net/minecraft/world/entity/projectile/FishingHook.java
+++ b/net/minecraft/world/entity/projectile/FishingHook.java
@@ -50,26 +_,42 @@
     private static final Logger LOGGER = LogUtils.getLogger();
     private final RandomSource syncronizedRandom = RandomSource.create();
     private boolean biting;
-    private int outOfWaterTime;
+    public int outOfWaterTime;
     private static final int MAX_OUT_OF_WATER_TIME = 10;
-    private static final EntityDataAccessor<Integer> DATA_HOOKED_ENTITY = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.INT);
+    public static final EntityDataAccessor<Integer> DATA_HOOKED_ENTITY = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.INT);
     private static final EntityDataAccessor<Boolean> DATA_BITING = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.BOOLEAN);
     private int life;
     private int nibble;
-    private int timeUntilLured;
-    private int timeUntilHooked;
-    private float fishAngle;
+    public int timeUntilLured;
+    public int timeUntilHooked;
+    public float fishAngle;
     private boolean openWater = true;
     @Nullable
-    private Entity hookedIn;
-    private FishingHook.FishHookState currentState = FishingHook.FishHookState.FLYING;
+    public Entity hookedIn;
+    public FishingHook.FishHookState currentState = FishingHook.FishHookState.FLYING;
     private final int luck;
     private final int lureSpeed;
 
+    // CraftBukkit start - Extra variables to enable modification of fishing wait time, values are minecraft defaults
+    public int minWaitTime = 100;
+    public int maxWaitTime = 600;
+    public int minLureTime = 20;
+    public int maxLureTime = 80;
+    public float minLureAngle = 0.0F;
+    public float maxLureAngle = 360.0F;
+    public boolean applyLure = true;
+    public boolean rainInfluenced = true;
+    public boolean skyInfluenced = true;
+    // CraftBukkit end
+
     private FishingHook(EntityType<? extends FishingHook> entityType, Level level, int luck, int lureSpeed) {
         super(entityType, level);
         this.luck = Math.max(0, luck);
         this.lureSpeed = Math.max(0, lureSpeed);
+        // Paper start - Configurable fishing time ranges
+        this.minWaitTime = level.paperConfig().fishingTimeRange.minimum;
+        this.maxWaitTime = level.paperConfig().fishingTimeRange.maximum;
+        // Paper end - Configurable fishing time ranges
     }
 
     public FishingHook(EntityType<? extends FishingHook> entityType, Level level) {
@@ -147,12 +_,12 @@
         super.tick();
         Player playerOwner = this.getPlayerOwner();
         if (playerOwner == null) {
-            this.discard();
+            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
         } else if (this.level().isClientSide || !this.shouldStopFishing(playerOwner)) {
             if (this.onGround()) {
                 this.life++;
                 if (this.life >= 1200) {
-                    this.discard();
+                    this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
                     return;
                 }
             } else {
@@ -251,14 +_,14 @@
         if (!player.isRemoved() && player.isAlive() && (isFishingRod || isFishingRod1) && !(this.distanceToSqr(player) > 1024.0)) {
             return false;
         } else {
-            this.discard();
+            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
             return true;
         }
     }
 
     private void checkCollision() {
         HitResult hitResultOnMoveVector = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity);
-        this.hitTargetOrDeflectSelf(hitResultOnMoveVector);
+        this.preHitTargetOrDeflectSelf(hitResultOnMoveVector);
     }
 
     @Override
@@ -280,7 +_,7 @@
         this.setDeltaMovement(this.getDeltaMovement().normalize().scale(result.distanceTo(this)));
     }
 
-    private void setHookedEntity(@Nullable Entity hookedEntity) {
+    public void setHookedEntity(@Nullable Entity hookedEntity) {
         this.hookedIn = hookedEntity;
         this.getEntityData().set(DATA_HOOKED_ENTITY, hookedEntity == null ? 0 : hookedEntity.getId() + 1);
     }
@@ -289,11 +_,11 @@
         ServerLevel serverLevel = (ServerLevel)this.level();
         int i = 1;
         BlockPos blockPos = pos.above();
-        if (this.random.nextFloat() < 0.25F && this.level().isRainingAt(blockPos)) {
+        if (this.rainInfluenced && this.random.nextFloat() < 0.25F && this.level().isRainingAt(blockPos)) { // CraftBukkit
             i++;
         }
 
-        if (this.random.nextFloat() < 0.5F && !this.level().canSeeSky(blockPos)) {
+        if (this.skyInfluenced && this.random.nextFloat() < 0.5F && !this.level().canSeeSky(blockPos)) { // CraftBukkit
             i--;
         }
 
@@ -303,6 +_,10 @@
                 this.timeUntilLured = 0;
                 this.timeUntilHooked = 0;
                 this.getEntityData().set(DATA_BITING, false);
+                // CraftBukkit start
+                org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) this.getPlayerOwner().getBukkitEntity(), null, (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.event.player.PlayerFishEvent.State.FAILED_ATTEMPT);
+                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
+                // CraftBukkit end
             }
         } else if (this.timeUntilHooked > 0) {
             this.timeUntilHooked -= i;
@@ -326,6 +_,13 @@
                     serverLevel.sendParticles(ParticleTypes.FISHING, d, d1, d2, 0, -f2, 0.01, f1, 1.0);
                 }
             } else {
+                // CraftBukkit start
+                org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) this.getPlayerOwner().getBukkitEntity(), null, (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.event.player.PlayerFishEvent.State.BITE);
+                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
+                if (playerFishEvent.isCancelled()) {
+                    return;
+                }
+                // CraftBukkit end
                 this.playSound(SoundEvents.FISHING_BOBBER_SPLASH, 0.25F, 1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.4F);
                 double d3 = this.getY() + 0.5;
                 serverLevel.sendParticles(
@@ -377,16 +_,35 @@
             }
 
             if (this.timeUntilLured <= 0) {
-                this.fishAngle = Mth.nextFloat(this.random, 0.0F, 360.0F);
-                this.timeUntilHooked = Mth.nextInt(this.random, 20, 80);
+                // CraftBukkit start - logic to modify fishing wait time, lure time, and lure angle
+                this.fishAngle = Mth.nextFloat(this.random, this.minLureAngle, this.maxLureAngle);
+                this.timeUntilHooked = Mth.nextInt(this.random, this.minLureTime, this.maxLureTime);
+                // CraftBukkit end
+                // Paper start - Add missing fishing event state
+                if (this.getPlayerOwner() != null) {
+                    org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) this.getPlayerOwner().getBukkitEntity(), null, (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.event.player.PlayerFishEvent.State.LURED);
+                    if (!playerFishEvent.callEvent()) {
+                        this.timeUntilHooked = 0;
+                        return;
+                    }
+                }
+                // Paper end - Add missing fishing event state
             }
         } else {
-            this.timeUntilLured = Mth.nextInt(this.random, 100, 600);
-            this.timeUntilLured = this.timeUntilLured - this.lureSpeed;
+            // CraftBukkit start - logic to modify fishing wait time
+            this.resetTimeUntilLured(); // Paper - more projectile api - extract time until lured reset logic
+            // CraftBukkit end
         }
     }
 
-    private boolean calculateOpenWater(BlockPos pos) {
+    // Paper start - more projectile api - extract time until lured reset logic
+    public void resetTimeUntilLured() {
+        this.timeUntilLured = Mth.nextInt(this.random, this.minWaitTime, this.maxWaitTime);
+        this.timeUntilLured -= (this.applyLure) ? (this.lureSpeed >= this.maxWaitTime ? this.timeUntilLured - 1 : this.lureSpeed ) : 0; // Paper - Fix Lure infinite loop
+    }
+    // Paper end - more projectile api - extract time until lured reset logic
+
+    public boolean calculateOpenWater(BlockPos pos) {
         FishingHook.OpenWaterType openWaterType = FishingHook.OpenWaterType.INVALID;
 
         for (int i = -1; i <= 2; i++) {
@@ -443,15 +_,34 @@
     public void readAdditionalSaveData(CompoundTag compound) {
     }
 
+
+    // Paper start - Add hand parameter to PlayerFishEvent
+    @Deprecated
+    @io.papermc.paper.annotation.DoNotUse
     public int retrieve(ItemStack stack) {
+        return this.retrieve(net.minecraft.world.InteractionHand.MAIN_HAND, stack);
+    }
+
+    public int retrieve(net.minecraft.world.InteractionHand hand, ItemStack stack) {
+        // Paper end - Add hand parameter to PlayerFishEvent
         Player playerOwner = this.getPlayerOwner();
         if (!this.level().isClientSide && playerOwner != null && !this.shouldStopFishing(playerOwner)) {
             int i = 0;
             if (this.hookedIn != null) {
+                // CraftBukkit start
+                org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) playerOwner.getBukkitEntity(), this.hookedIn.getBukkitEntity(), (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), org.bukkit.event.player.PlayerFishEvent.State.CAUGHT_ENTITY); // Paper - Add hand parameter to PlayerFishEvent
+                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
+
+                if (playerFishEvent.isCancelled()) {
+                    return 0;
+                }
+                if (this.hookedIn != null) { // Paper - re-check to see if there is a hooked entity
+                // CraftBukkit end
                 this.pullEntity(this.hookedIn);
                 CriteriaTriggers.FISHING_ROD_HOOKED.trigger((ServerPlayer)playerOwner, stack, this, Collections.emptyList());
                 this.level().broadcastEntityEvent(this, (byte)31);
                 i = this.hookedIn instanceof ItemEntity ? 3 : 5;
+                } // Paper - re-check to see if there is a hooked entity
             } else if (this.nibble > 0) {
                 LootParams lootParams = new LootParams.Builder((ServerLevel)this.level())
                     .withParameter(LootContextParams.ORIGIN, this.position())
@@ -465,18 +_,32 @@
 
                 for (ItemStack itemStack : randomItems) {
                     ItemEntity itemEntity = new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), itemStack);
+                    // CraftBukkit start
+                    org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) playerOwner.getBukkitEntity(), itemEntity.getBukkitEntity(), (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), org.bukkit.event.player.PlayerFishEvent.State.CAUGHT_FISH); // Paper - itemEntity may be null // Paper - Add hand parameter to PlayerFishEvent
+                    playerFishEvent.setExpToDrop(this.random.nextInt(6) + 1);
+                    this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
+
+                    if (playerFishEvent.isCancelled()) {
+                        return 0;
+                    }
+                    // CraftBukkit end
                     double d = playerOwner.getX() - this.getX();
                     double d1 = playerOwner.getY() - this.getY();
                     double d2 = playerOwner.getZ() - this.getZ();
                     double d3 = 0.1;
                     itemEntity.setDeltaMovement(d * 0.1, d1 * 0.1 + Math.sqrt(Math.sqrt(d * d + d1 * d1 + d2 * d2)) * 0.08, d2 * 0.1);
                     this.level().addFreshEntity(itemEntity);
-                    playerOwner.level()
-                        .addFreshEntity(
-                            new ExperienceOrb(
-                                playerOwner.level(), playerOwner.getX(), playerOwner.getY() + 0.5, playerOwner.getZ() + 0.5, this.random.nextInt(6) + 1
-                            )
-                        );
+                    // CraftBukkit start - this.random.nextInt(6) + 1 -> playerFishEvent.getExpToDrop()
+                    if (playerFishEvent.getExpToDrop() > 0) {
+                        playerOwner.level()
+                            .addFreshEntity(
+                                new ExperienceOrb(
+                                    playerOwner.level(), playerOwner.getX(), playerOwner.getY() + 0.5, playerOwner.getZ() + 0.5, playerFishEvent.getExpToDrop(), org.bukkit.entity.ExperienceOrb.SpawnReason.FISHING, this.getPlayerOwner(), this // Paper
+                                )
+                            );
+                    }
+                    // CraftBukkit end
+
                     if (itemStack.is(ItemTags.FISHES)) {
                         playerOwner.awardStat(Stats.FISH_CAUGHT, 1);
                     }
@@ -486,10 +_,27 @@
             }
 
             if (this.onGround()) {
+                // CraftBukkit start
+                org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) playerOwner.getBukkitEntity(), null, (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), org.bukkit.event.player.PlayerFishEvent.State.IN_GROUND); // Paper - Add hand parameter to PlayerFishEvent
+                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
+
+                if (playerFishEvent.isCancelled()) {
+                    return 0;
+                }
+                // CraftBukkit end
                 i = 2;
             }
+            // CraftBukkit start
+            if (i == 0) {
+                org.bukkit.event.player.PlayerFishEvent playerFishEvent = new org.bukkit.event.player.PlayerFishEvent((org.bukkit.entity.Player) playerOwner.getBukkitEntity(), null, (org.bukkit.entity.FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), org.bukkit.event.player.PlayerFishEvent.State.REEL_IN); // Paper - Add hand parameter to PlayerFishEvent
+                this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent);
+                if (playerFishEvent.isCancelled()) {
+                    return 0;
+                }
+            }
+            // CraftBukkit end
 
-            this.discard();
+            this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause
             return i;
         } else {
             return 0;
@@ -505,7 +_,7 @@
         super.handleEntityEvent(id);
     }
 
-    protected void pullEntity(Entity entity) {
+    public void pullEntity(Entity entity) {
         Entity owner = this.getOwner();
         if (owner != null) {
             Vec3 vec3 = new Vec3(owner.getX() - this.getX(), owner.getY() - this.getY(), owner.getZ() - this.getZ()).scale(0.1);
@@ -520,8 +_,15 @@
 
     @Override
     public void remove(Entity.RemovalReason reason) {
+        // CraftBukkit start - add Bukkit remove cause
+        this.remove(reason, null);
+    }
+
+    @Override
+    public void remove(Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.Cause cause) {
+        // CraftBukkit end
         this.updateOwnerInfo(null);
-        super.remove(reason);
+        super.remove(reason, cause); // CraftBukkit - add Bukkit remove cause
     }
 
     @Override
@@ -570,11 +_,11 @@
         if (this.getPlayerOwner() == null) {
             int data = packet.getData();
             LOGGER.error("Failed to recreate fishing hook on client. {} (id: {}) is not a valid owner.", this.level().getEntity(data), data);
-            this.discard();
+            this.discard(null); // CraftBukkit - add Bukkit remove cause
         }
     }
 
-    static enum FishHookState {
+    public static enum FishHookState {
         FLYING,
         HOOKED_IN_ENTITY,
         BOBBING;
