mojira.dev
MC-13518

You can switch fishing rods while the line is still cast

The Bug:

You can switch fishing rods while the line is still cast.

Steps to Reproduce:

  1. Obtain two fishing rods and place them next to one another in your hotbar.

  2. Use one of the fishing rods.

  3. Switch to the other fishing rod.

  4. Take note as to whether or not you can switch fishing rods while the line is still cast.

Observed Behavior:

You can switch fishing rods while the line is still cast.

Expected Behavior:

You would not be able to switch fishing rods while the line is still cast.

Expected Behavior:

You would not be able to switch fishing rods while the line is still cast.

Code Analysis & Fix:

Code analysis and fix by @unknown can be found in this comment.

Linked issues

Attachments

Comments 36

Tails

Confirmed, the switched Fishing rod takes damaged from the cast.

Cammy Dunn

Yay 😃 My first confirmed issue!

David Harmon

Not only does the line stay cast, but after being signalled that you have a fish on the line, you can switch to a different rod and reel in the fish with that one! Also, confirming in 1.6.1 .

Anomonous Anomonous

Does it affect 1.9? Also , if one fishing rod has luck of the sea 1 and the other has lure 1 , will the fish you catch be affected by lure or luck of the sea?

marcono1234

Confirmed for

  • 16w15b Also affects switching with offhand

The fishing rod with which you stop fishing gets damaged and probably affects the fished item as well

26 more comments
Avoma

Can confirm in 21w17a.

Avoma

I'd like to request ownership of this ticket since the current reporter has been inactive since October 2018. I'm willing to provide all of the necessary information and will keep this report updated.

Avoma

Can confirm in 1.17.

NBG-bootmgr

Can confirm in 22w17a.

Apollo30

Code Analysis:

The following is based on a decompiled version of Minecraft 1.20.1 using MCP-Reborn.

net.minecraft.world.entity.projectile.FishingHook.java // Original

public class FishingHook extends Projectile {
    ...
    public void tick() {
        this.syncronizedRandom.setSeed(this.getUUID().getLeastSignificantBits() ^ this.level().getGameTime());
        super.tick();
        Player player = this.getPlayerOwner();
        if (player == null) {
            this.discard();
        } else if (this.level().isClientSide || !this.shouldStopFishing(player)) {
            if (this.onGround()) {
                ++this.life;
                if (this.life >= 1200) {
                    this.discard();
                    return;
                }
            } else {
                this.life = 0;
            }
            ...
        }
        ...
    }
    ...
}

net.minecraft.world.item.FishingRodItem.java // Original

public class FishingRodItem extends Item implements Vanishable {
    ...
    public InteractionResultHolder < ItemStack > use(Level p_41290_, Player p_41291_, InteractionHand p_41292_) {
        ItemStack itemstack = p_41291_.getItemInHand(p_41292_);
        if (p_41291_.fishing != null) {
            if (!p_41290_.isClientSide) {
                int i = p_41291_.fishing.retrieve(itemstack);
                itemstack.hurtAndBreak(i, p_41291_, (p_41288_) - > {
                    p_41288_.broadcastBreakEvent(p_41292_);
                });
            }            p_41290_.playSound((Player) null, p_41291_.getX(), p_41291_.getY(), p_41291_.getZ(), SoundEvents.FISHING_BOBBER_RETRIEVE, SoundSource.NEUTRAL, 1.0 F, 0.4 F / (p_41290_.getRandom().nextFloat() * 0.4 F + 0.8 F));
            p_41291_.gameEvent(GameEvent.ITEM_INTERACT_FINISH);
        } else {
            p_41290_.playSound((Player) null, p_41291_.getX(), p_41291_.getY(), p_41291_.getZ(), SoundEvents.FISHING_BOBBER_THROW, SoundSource.NEUTRAL, 0.5 F, 0.4 F / (p_41290_.getRandom().nextFloat() * 0.4 F + 0.8 F));
            if (!p_41290_.isClientSide) {
                int k = EnchantmentHelper.getFishingSpeedBonus(itemstack);
                int j = EnchantmentHelper.getFishingLuckBonus(itemstack);
                p_41290_.addFreshEntity(new FishingHook(p_41291_, p_41290_, j, k));
            }            p_41291_.awardStat(Stats.ITEM_USED.get(this));
            p_41291_.gameEvent(GameEvent.ITEM_INTERACT_START);
        }        return InteractionResultHolder.sidedSuccess(itemstack, p_41290_.isClientSide());
    }
    ...
}

net.minecraft.world.entity.LivingEntity.java // Original

public abstract class LivingEntity extends Entity implements Attackable {
    ...
    private void handleHandSwap(Map < EquipmentSlot, ItemStack > p_21092_) {
        ItemStack itemstack = p_21092_.get(EquipmentSlot.MAINHAND);
        ItemStack itemstack1 = p_21092_.get(EquipmentSlot.OFFHAND);
        if (itemstack != null && itemstack1 != null && ItemStack.matches(itemstack, this.getLastHandItem(EquipmentSlot.OFFHAND)) && ItemStack.matches(itemstack1, this.getLastHandItem(EquipmentSlot.MAINHAND))) {
            ((ServerLevel) this.level()).getChunkSource().broadcast(this, new ClientboundEntityEventPacket(this, (byte) 55));
            p_21092_.remove(EquipmentSlot.MAINHAND);
            p_21092_.remove(EquipmentSlot.OFFHAND);
            this.setLastHandItem(EquipmentSlot.MAINHAND, itemstack.copy());
            this.setLastHandItem(EquipmentSlot.OFFHAND, itemstack1.copy());
        }
    }
    ...
}

These are the 3 original classes that will relate to the fix of most fishing rod swapping bugs. As I can tell, we aren't keeping track of the fishing rod being used. Therefore, we can swap out a secondary fishing rod, which then swaps the fishing line to that secondary fishing rod. My original plan was to keep track of the itemstack being used when casting the fishing rod, BUT I couldn't find a method to separate two similar itemstacks. Therefore, I had to resort to keeping track of the slot of the casted fishing rod.  

Fix:

net.minecraft.world.entity.projectile.FishingHook.java // Updated

public class FishingHook extends Projectile {
    ...
    private int rodSlot; // -1 means offhand, -2 means spawned
    ...
    private FishingHook(EntityType << ? extends FishingHook > p_150141_, Level p_150142_, int rodSlot, int p_150143_, int p_150144_) { // added rodSlot
        super(p_150141_, p_150142_);
        this.noCulling = true;
        this.luck = Math.max(0, p_150143_);
        this.lureSpeed = Math.max(0, p_150144_);
        this.rodSlot = rodSlot; // added line
    }    public FishingHook(EntityType << ? extends FishingHook > p_150138_, Level p_150139_) {
        this(p_150138_, p_150139_, -2, 0, 0); // added parameter for rodSlot
    }    public FishingHook(Player p_37106_, Level p_37107_, int rodSlot, int p_37108_, int p_37109_) { // added rodSlot
        this(EntityType.FISHING_BOBBER, p_37107_, rodSlot, p_37108_, p_37109_); // added parameter for rodSlot
        this.setOwner(p_37106_);
        float f = p_37106_.getXRot();
        float f1 = p_37106_.getYRot();
        float f2 = Mth.cos(-f1 * ((float) Math.PI / 180 F) - (float) Math.PI);
        float f3 = Mth.sin(-f1 * ((float) Math.PI / 180 F) - (float) Math.PI);
        float f4 = -Mth.cos(-f * ((float) Math.PI / 180 F));
        float f5 = Mth.sin(-f * ((float) Math.PI / 180 F));
        double d0 = p_37106_.getX() - (double) f3 * 0.3 D;
        double d1 = p_37106_.getEyeY();
        double d2 = p_37106_.getZ() - (double) f2 * 0.3 D;
        this.moveTo(d0, d1, d2, f1, f);
        Vec3 vec3 = new Vec3((double)(-f3), (double) Mth.clamp(-(f5 / f4), -5.0 F, 5.0 F), (double)(-f2));
        double d3 = vec3.length();
        vec3 = vec3.multiply(0.6 D / d3 + this.random.triangle(0.5 D, 0.0103365 D), 0.6 D / d3 + this.random.triangle(0.5 D, 0.0103365 D), 0.6 D / d3 + this.random.triangle(0.5 D, 0.0103365 D));
        this.setDeltaMovement(vec3);
        this.setYRot((float)(Mth.atan2(vec3.x, vec3.z) * (double)(180 F / (float) Math.PI)));
        this.setXRot((float)(Mth.atan2(vec3.y, vec3.horizontalDistance()) * (double)(180 F / (float) Math.PI)));
        this.yRotO = this.getYRot();
        this.xRotO = this.getXRot();
    }
    ...
    public void tick() {
        this.syncronizedRandom.setSeed(this.getUUID().getLeastSignificantBits() ^ this.level().getGameTime());
        super.tick();
        Player player = this.getPlayerOwner();
        if (player == null) {
            this.discard();
        } else {
            Inventory inventory = player.getInventory();
            if (this.rodSlot == -1 && (inventory.getItem(inventory.selected).is(Items.FISHING_ROD)) || this.rodSlot >= 0 && (this.rodSlot != inventory.selected || !inventory.getItem(inventory.selected).is(Items.FISHING_ROD)))
                this.discard();
            else if (this.level().isClientSide || !this.shouldStopFishing(player)) {
                if (this.onGround()) {
                    ++this.life;
                    if (this.life >= 1200) {
                        this.discard();
                        return;
                    }
                } else {
                    this.life = 0;
                }
                ...
            }
            ...
        }
        ...
    }
    ...
}

In this modified version of FishingHook.java, we now keep track of where the fishing rod is located in the player's inventory. Whether if it was spawned in, casted from the offhand, or casted from the main hand. Then, we simply check the slot of where the rod is if the fishing rod is still in that original slot. 

After we've solved most of the fishing rod swapping bugs, we now have to alter other classes to make it compatible with our changes.

net.minecraft.world.item.FishingRodItem.java // Updated

public class FishingRodItem extends Item implements Vanishable {
    ...
    public InteractionResultHolder < ItemStack > use(Level level, Player player, InteractionHand interactionHand) {        ItemStack itemstack = player.getItemInHand(interactionHand);
        if (player.fishing != null) {
            if (!level.isClientSide) {
                int i = player.fishing.retrieve(itemstack);
                itemstack.hurtAndBreak(i, player, (p_41288_) - > {
                    p_41288_.broadcastBreakEvent(interactionHand);
                });
            }            level.playSound((Player) null, player.getX(), player.getY(), player.getZ(), SoundEvents.FISHING_BOBBER_RETRIEVE, SoundSource.NEUTRAL, 1.0 F, 0.4 F / (level.getRandom().nextFloat() * 0.4 F + 0.8 F));
            player.gameEvent(GameEvent.ITEM_INTERACT_FINISH);
        } else {
            level.playSound((Player) null, player.getX(), player.getY(), player.getZ(), SoundEvents.FISHING_BOBBER_THROW, SoundSource.NEUTRAL, 0.5 F, 0.4 F / (level.getRandom().nextFloat() * 0.4 F + 0.8 F));
            if (!level.isClientSide) {
                int k = EnchantmentHelper.getFishingSpeedBonus(itemstack);
                int j = EnchantmentHelper.getFishingLuckBonus(itemstack);
                level.addFreshEntity(new FishingHook(player, level, interactionHand.equals(InteractionHand.OFF_HAND) ? -1 : player.getInventory().selected, j, k));
            }            player.awardStat(Stats.ITEM_USED.get(this));
            player.gameEvent(GameEvent.ITEM_INTERACT_START);
        }        return InteractionResultHolder.sidedSuccess(itemstack, level.isClientSide());
    }
    ...
}

Our last problem to solve, is to uncast fishing rods if two separate rods are being swapped via offhand and mainhand. But, if we are transferring a fishing rod from the offhand to mainhand, it will update the selected slot of the fishing rod. This will also work vice versa.

net.minecraft.world.entity.LivingEntity.java // Updated

public abstract class LivingEntity extends Entity implements Attackable {
    ...
    private void handleHandSwap(Map < EquipmentSlot, ItemStack > p_21092_) {
        ItemStack itemstack = p_21092_.get(EquipmentSlot.MAINHAND);
        ItemStack itemstack1 = p_21092_.get(EquipmentSlot.OFFHAND);
        if (itemstack != null && itemstack1 != null && ItemStack.matches(itemstack, this.getLastHandItem(EquipmentSlot.OFFHAND)) && ItemStack.matches(itemstack1, this.getLastHandItem(EquipmentSlot.MAINHAND))) {
            ((ServerLevel) this.level()).getChunkSource().broadcast(this, new ClientboundEntityEventPacket(this, (byte) 55));
            p_21092_.remove(EquipmentSlot.MAINHAND);
            p_21092_.remove(EquipmentSlot.OFFHAND);
            this.setLastHandItem(EquipmentSlot.MAINHAND, itemstack.copy());
            this.setLastHandItem(EquipmentSlot.OFFHAND, itemstack1.copy());
        }        

        if (this instanceof Player player) {
            if (player.fishing != null) {
                if (itemstack1 != null && itemstack != null && itemstack.is(Items.FISHING_ROD) && itemstack1.is(Items.FISHING_ROD))
                    player.fishing.discard();
                else
                    player.fishing.setRodSlot(player.getInventory().selected);
            }
        }
    }
    ...
}

This code analysis is not an official solution to resolve the fishing rod swapping bug.

But it sure does give an idea on how it SHOULD be solved.

Thank you for tuning into my TED talk!

Cammy Dunn

Avoma

(Unassigned)

Confirmed

Gameplay

Normal

Items

fishing_rod

Minecraft 1.5.1, Minecraft 1.8, Minecraft 1.8.1-pre3, Minecraft 1.8.7, Minecraft 1.8.8, ..., 1.21.2 Pre-Release 3, 1.21.3, 1.21.4, 25w08a, 1.21.5

Retrieved