mojira.dev

Apollo30

Assigned

No issues.

Reported

No issues.

Comments

Amazing, it's finally been resolved!

Fix:

The following is based on a decompiled version of Minecraft 1.20.1 using MCP-Reborn.
This is a solution to fix the problem of tamed cats panicking whenever their owner attacked it.   

net.minecraft.world.entity.animal.Cat.java

protected void registerGoals() {
   this.temptGoal = new Cat.CatTemptGoal(this, 0.6D, TEMPT_INGREDIENT, true);
   this.goalSelector.addGoal(1, new FloatGoal(this));
   this.goalSelector.addGoal(1, new CatPanicGoal(1.5D));
   ...
}

...

class CatPanicGoal extends PanicGoal {
   public CatPanicGoal(double p_203124_) {
      super(Cat.this, p_203124_);
   }

   protected boolean shouldPanic() {
      return (this.mob.getLastHurtByMob() != null && !this.mob.getLastHurtByMob().equals(Cat.this.getOwner())) || this.mob.isFreezing() || this.mob.isOnFire();
   }
}

The old Cat.java file used the normal PanicGoal which determines if the entity should panic.

In it, it checked if this.mob.getLastHurtByMob() != null but never checked if the damager is the owner.

Code Analysis:

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

net.minecraft.client.model.HumanoidModel.java // Original

private void poseRightArm(T p_102876_) {
    switch (this.rightArmPose) {
        case EMPTY:
            this.rightArm.yRot = 0.0F;
            break;
        case BLOCK:
            this.rightArm.xRot = this.rightArm.xRot * 0.5F - 0.9424779F;
            this.rightArm.yRot = (-(float) Math.PI / 6 F);
            break;
        case ITEM:
            this.rightArm.xRot = this.rightArm.xRot * 0.5F - ((float) Math.PI / 10F);
            this.rightArm.yRot = 0.0 F;
            break;
        case THROW_SPEAR:
            this.rightArm.xRot = this.rightArm.xRot * 0.5F - (float) Math.PI;
            this.rightArm.yRot = 0.0 F;
            break;
        case BOW_AND_ARROW:
            this.rightArm.yRot = -0.1F + this.head.yRot;
            this.leftArm.yRot = 0.1 F + this.head.yRot + 0.4F;
            this.rightArm.xRot = (-(float) Math.PI / 2F) + this.head.xRot;
            this.leftArm.xRot = (-(float) Math.PI / 2F) + this.head.xRot;
            break;
        case CROSSBOW_CHARGE:
            AnimationUtils.animateCrossbowCharge(this.rightArm, this.leftArm, p_102876_, true);
            break;
        case CROSSBOW_HOLD:
            AnimationUtils.animateCrossbowHold(this.rightArm, this.leftArm, this.head, true);
            break;
        case BRUSH:
            this.rightArm.xRot = this.rightArm.xRot * 0.5F - ((float) Math.PI / 5F);
            this.rightArm.yRot = 0.0F;
            break;
        case SPYGLASS:
            this.rightArm.xRot = Mth.clamp(this.head.xRot - 1.9198622F - (p_102876_.isCrouching() ? 0.2617994 F : 0.0 F), -2.4 F, 3.3 F);
            this.rightArm.yRot = this.head.yRot - 0.2617994F;
            break;
        case TOOT_HORN:
            this.rightArm.xRot = Mth.clamp(this.head.xRot, -1.2F, 1.2F) - 1.4835298F;
            this.rightArm.yRot = this.head.yRot - ((float) Math.PI / 6F);
    }}

private void poseLeftArm(T p_102879_) {
    switch (this.leftArmPose) {
        case EMPTY:
            this.leftArm.yRot = 0.0F;
            break;
        case BLOCK:
            this.leftArm.xRot = this.leftArm.xRot * 0.5F - 0.9424779F;
            this.leftArm.yRot = ((float) Math.PI / 6 F);
            break;
        case ITEM:
            this.leftArm.xRot = this.leftArm.xRot * 0.5F - ((float) Math.PI / 10F);
            this.leftArm.yRot = 0.0F;
            break;
        case THROW_SPEAR:
            this.leftArm.xRot = this.leftArm.xRot * 0.5F - (float) Math.PI;
            this.leftArm.yRot = 0.0F;
            break;
        case BOW_AND_ARROW:
            this.rightArm.yRot = -0.1F + this.head.yRot - 0.4F;
            this.leftArm.yRot = 0.1 F + this.head.yRot;
            this.rightArm.xRot = (-(float) Math.PI / 2 F) + this.head.xRot;
            this.leftArm.xRot = (-(float) Math.PI / 2 F) + this.head.xRot;
            break;
        case CROSSBOW_CHARGE:
            AnimationUtils.animateCrossbowCharge(this.rightArm, this.leftArm, p_102879_, false);
            break;
        case CROSSBOW_HOLD:
            AnimationUtils.animateCrossbowHold(this.rightArm, this.leftArm, this.head, false);
            break;
        case BRUSH:
            this.leftArm.xRot = this.leftArm.xRot * 0.5F - ((float) Math.PI / 5F);
            this.leftArm.yRot = 0.0F;
            break;
        case SPYGLASS:
            this.leftArm.xRot = Mth.clamp(this.head.xRot - 1.9198622F - (p_102879_.isCrouching() ? 0.2617994F : 0.0 F), -2.4F, 3.3F);
            this.leftArm.yRot = this.head.yRot + 0.2617994F;
            break;
        case TOOT_HORN:
            this.leftArm.xRot = Mth.clamp(this.head.xRot, -1.2 F, 1.2F) - 1.4835298F;
            this.leftArm.yRot = this.head.yRot + ((float) Math.PI / 6F);
    }
}

As seen in this code, we see the enum, BLOCK. When the switch case reaches that point, we do not see compatibility with crouching as opposed to the SPYGLASS enum.

Fix:

A simple fix is to copy the ternary operator from the SPYGLASS enum to the BLOCK enum.
Loading up 1.20.1 with MCP-Reborn, the trident's tilt will remain facing up.

net.minecraft.client.model.HumanoidModel.java // Updated

private void poseRightArm(T p_102876_) {
    switch (this.rightArmPose) {
        case EMPTY:
            this.rightArm.yRot = 0.0F;
            break;
        case BLOCK:
            this.rightArm.xRot = this.rightArm.xRot * 0.5F - 0.9424779F - (p_102879_.isCrouching() ? 0.2617994F : 0.0F);
            this.rightArm.yRot = (-(float) Math.PI / 6 F);
            break;
        case ITEM:
            this.rightArm.xRot = this.rightArm.xRot * 0.5F - ((float) Math.PI / 10F);
            this.rightArm.yRot = 0.0 F;
            break;
        case THROW_SPEAR:
            this.rightArm.xRot = this.rightArm.xRot * 0.5F - (float) Math.PI;
            this.rightArm.yRot = 0.0 F;
            break;
        case BOW_AND_ARROW:
            this.rightArm.yRot = -0.1F + this.head.yRot;
            this.leftArm.yRot = 0.1 F + this.head.yRot + 0.4F;
            this.rightArm.xRot = (-(float) Math.PI / 2F) + this.head.xRot;
            this.leftArm.xRot = (-(float) Math.PI / 2F) + this.head.xRot;
            break;
        case CROSSBOW_CHARGE:
            AnimationUtils.animateCrossbowCharge(this.rightArm, this.leftArm, p_102876_, true);
            break;
        case CROSSBOW_HOLD:
            AnimationUtils.animateCrossbowHold(this.rightArm, this.leftArm, this.head, true);
            break;
        case BRUSH:
            this.rightArm.xRot = this.rightArm.xRot * 0.5F - ((float) Math.PI / 5F);
            this.rightArm.yRot = 0.0F;
            break;
        case SPYGLASS:
            this.rightArm.xRot = Mth.clamp(this.head.xRot - 1.9198622F - (p_102876_.isCrouching() ? 0.2617994 F : 0.0 F), -2.4 F, 3.3 F);
            this.rightArm.yRot = this.head.yRot - 0.2617994F;
            break;
        case TOOT_HORN:
            this.rightArm.xRot = Mth.clamp(this.head.xRot, -1.2F, 1.2F) - 1.4835298F;
            this.rightArm.yRot = this.head.yRot - ((float) Math.PI / 6F);
    }}
private void poseLeftArm(T p_102879_) {
    switch (this.leftArmPose) {
        case EMPTY:
            this.leftArm.yRot = 0.0F;
            break;
        case BLOCK:
            this.leftArm.xRot = this.leftArm.xRot * 0.5F - 0.9424779F - (p_102879_.isCrouching() ? 0.2617994F : 0.0F);
            this.leftArm.yRot = ((float) Math.PI / 6 F);
            break;
        case ITEM:
            this.leftArm.xRot = this.leftArm.xRot * 0.5F - ((float) Math.PI / 10F);
            this.leftArm.yRot = 0.0F;
            break;
        case THROW_SPEAR:
            this.leftArm.xRot = this.leftArm.xRot * 0.5F - (float) Math.PI;
            this.leftArm.yRot = 0.0F;
            break;
        case BOW_AND_ARROW:
            this.rightArm.yRot = -0.1F + this.head.yRot - 0.4F;
            this.leftArm.yRot = 0.1 F + this.head.yRot;
            this.rightArm.xRot = (-(float) Math.PI / 2 F) + this.head.xRot;
            this.leftArm.xRot = (-(float) Math.PI / 2 F) + this.head.xRot;
            break;
        case CROSSBOW_CHARGE:
            AnimationUtils.animateCrossbowCharge(this.rightArm, this.leftArm, p_102879_, false);
            break;
        case CROSSBOW_HOLD:
            AnimationUtils.animateCrossbowHold(this.rightArm, this.leftArm, this.head, false);
            break;
        case BRUSH:
            this.leftArm.xRot = this.leftArm.xRot * 0.5F - ((float) Math.PI / 5F);
            this.leftArm.yRot = 0.0F;
            break;
        case SPYGLASS:
            this.leftArm.xRot = Mth.clamp(this.head.xRot - 1.9198622F - (p_102879_.isCrouching() ? 0.2617994F : 0.0 F), -2.4F, 3.3F);
            this.leftArm.yRot = this.head.yRot + 0.2617994F;
            break;
        case TOOT_HORN:
            this.leftArm.xRot = Mth.clamp(this.head.xRot, -1.2 F, 1.2F) - 1.4835298F;
            this.leftArm.yRot = this.head.yRot + ((float) Math.PI / 6F);
    }
}

 

Fix:

The following is based on a decompiled version of Minecraft 1.20.1 using MCP-Reborn.
To fix this, we will have to determine if the LivingEntity is crouching. If so, we will add 0.35157994 to the x and y rot.

net.minecraft.client.model.AnimationUtils.java // Updated

@OnlyIn(Dist.CLIENT)
public class AnimationUtils {
    public static void animateCrossbowHold(ModelPart p_102098_, ModelPart p_102099_, ModelPart p_102100_, LivingEntity livingEntity, boolean p_102101_) {
        ModelPart modelpart = p_102101_ ? p_102098_ : p_102099_;
        ModelPart modelpart1 = p_102101_ ? p_102099_ : p_102098_;
        modelpart.yRot = (p_102101_ ? -0.3 F : 0.3 F) + p_102100_.yRot;
        modelpart1.yRot = (p_102101_ ? 0.6 F : -0.6 F) + p_102100_.yRot;
        modelpart.xRot = (-(float) Math.PI / 2 F) + p_102100_.xRot + 0.1 F - (livingEntity.isCrouching() ? 0.3517994 F : 0.0 F);
        modelpart1.xRot = -1.5 F + p_102100_.xRot - (livingEntity.isCrouching() ? 0.3517994 F : 0.0 F);
    }    

    public static void animateCrossbowCharge(ModelPart p_102087_, ModelPart p_102088_, LivingEntity p_102089_, boolean p_102090_) {
        ModelPart modelpart = p_102090_ ? p_102087_ : p_102088_;
        ModelPart modelpart1 = p_102090_ ? p_102088_ : p_102087_;
        modelpart.yRot = p_102090_ ? -0.8 F : 0.8 F;
        modelpart.xRot = -0.97079635 F - (p_102089_.isCrouching() ? 0.3517994 F : 0.0 F);
        modelpart1.xRot = modelpart.xRot - (p_102089_.isCrouching() ? 0.3517994 F : 0.0 F);
        float f = (float) CrossbowItem.getChargeDuration(p_102089_.getUseItem());
        float f1 = Mth.clamp((float) p_102089_.getTicksUsingItem(), 0.0 F, f);
        float f2 = f1 / f;
        modelpart1.yRot = Mth.lerp(f2, 0.4 F, 0.85 F) * (float)(p_102090_ ? 1 : -1);
        modelpart1.xRot = Mth.lerp(f2, modelpart1.xRot, (-(float) Math.PI / 2 F));
    }
}

 

Since we added another parameter to the method, we must update any references to this method.
As an example:

net.minecraft.client.model.HumanoidModel.java // Updated

AnimationUtils.animateCrossbowHold(this.rightArm, this.leftArm, this.head, livingEntity, true);

There if you were to crouch whilst holding or charging up a crossbow, the elevation should remain consistent.

Yet again, we have another underlying issue of the opposite hand not connecting when looking up or down.

Code Analysis:

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

net.minecraft.client.model.HumanoidModel.java // Original

private void poseRightArm(T p_102876_) {
    switch (this.rightArmPose) {
        case EMPTY:
            this.rightArm.yRot = 0.0F;
            break;
        case BLOCK:
            this.rightArm.xRot = this.rightArm.xRot * 0.5F - 0.9424779F;
            this.rightArm.yRot = (-(float) Math.PI / 6 F);
            break;
        case ITEM:
            this.rightArm.xRot = this.rightArm.xRot * 0.5F - ((float) Math.PI / 10F);
            this.rightArm.yRot = 0.0 F;
            break;
        case THROW_SPEAR:
            this.rightArm.xRot = this.rightArm.xRot * 0.5F - (float) Math.PI;
            this.rightArm.yRot = 0.0 F;
            break;
        case BOW_AND_ARROW:
            this.rightArm.yRot = -0.1F + this.head.yRot;
            this.leftArm.yRot = 0.1 F + this.head.yRot + 0.4F;
            this.rightArm.xRot = (-(float) Math.PI / 2F) + this.head.xRot;
            this.leftArm.xRot = (-(float) Math.PI / 2F) + this.head.xRot;
            break;
        case CROSSBOW_CHARGE:
            AnimationUtils.animateCrossbowCharge(this.rightArm, this.leftArm, p_102876_, true);
            break;
        case CROSSBOW_HOLD:
            AnimationUtils.animateCrossbowHold(this.rightArm, this.leftArm, this.head, true);
            break;
        case BRUSH:
            this.rightArm.xRot = this.rightArm.xRot * 0.5F - ((float) Math.PI / 5F);
            this.rightArm.yRot = 0.0F;
            break;
        case SPYGLASS:
            this.rightArm.xRot = Mth.clamp(this.head.xRot - 1.9198622F - (p_102876_.isCrouching() ? 0.2617994 F : 0.0 F), -2.4 F, 3.3 F);
            this.rightArm.yRot = this.head.yRot - 0.2617994F;
            break;
        case TOOT_HORN:
            this.rightArm.xRot = Mth.clamp(this.head.xRot, -1.2F, 1.2F) - 1.4835298F;
            this.rightArm.yRot = this.head.yRot - ((float) Math.PI / 6F);
    }}private void poseLeftArm(T p_102879_) {
    switch (this.leftArmPose) {
        case EMPTY:
            this.leftArm.yRot = 0.0F;
            break;
        case BLOCK:
            this.leftArm.xRot = this.leftArm.xRot * 0.5F - 0.9424779F;
            this.leftArm.yRot = ((float) Math.PI / 6 F);
            break;
        case ITEM:
            this.leftArm.xRot = this.leftArm.xRot * 0.5F - ((float) Math.PI / 10F);
            this.leftArm.yRot = 0.0F;
            break;
        case THROW_SPEAR:
            this.leftArm.xRot = this.leftArm.xRot * 0.5F - (float) Math.PI;
            this.leftArm.yRot = 0.0F;
            break;
        case BOW_AND_ARROW:
            this.rightArm.yRot = -0.1F + this.head.yRot - 0.4F;
            this.leftArm.yRot = 0.1 F + this.head.yRot;
            this.rightArm.xRot = (-(float) Math.PI / 2 F) + this.head.xRot;
            this.leftArm.xRot = (-(float) Math.PI / 2 F) + this.head.xRot;
            break;
        case CROSSBOW_CHARGE:
            AnimationUtils.animateCrossbowCharge(this.rightArm, this.leftArm, p_102879_, false);
            break;
        case CROSSBOW_HOLD:
            AnimationUtils.animateCrossbowHold(this.rightArm, this.leftArm, this.head, false);
            break;
        case BRUSH:
            this.leftArm.xRot = this.leftArm.xRot * 0.5F - ((float) Math.PI / 5F);
            this.leftArm.yRot = 0.0F;
            break;
        case SPYGLASS:
            this.leftArm.xRot = Mth.clamp(this.head.xRot - 1.9198622F - (p_102879_.isCrouching() ? 0.2617994F : 0.0 F), -2.4F, 3.3F);
            this.leftArm.yRot = this.head.yRot + 0.2617994F;
            break;
        case TOOT_HORN:
            this.leftArm.xRot = Mth.clamp(this.head.xRot, -1.2 F, 1.2F) - 1.4835298F;
            this.leftArm.yRot = this.head.yRot + ((float) Math.PI / 6F);
    }
}

As seen in this code, we see the enum, THROW_SPEAR. When the switch case reaches that point, we do not see compatibility with crouching as opposed to the SPYGLASS enum.

Fix:

A simple fix is to copy the ternary operator from the SPYGLASS enum to the THROW_SPEAR enum.
Loading up 1.20.1 with MCP-Reborn, the trident's tilt will remain facing up.

net.minecraft.client.model.HumanoidModel.java // Updated

private void poseRightArm(T p_102876_) {
    switch (this.rightArmPose) {
        case EMPTY:
            this.rightArm.yRot = 0.0 F;
            break;
        case BLOCK:
            this.rightArm.xRot = this.rightArm.xRot * 0.5 F - 0.9424779 F;
            this.rightArm.yRot = (-(float) Math.PI / 6 F);
            break;
        case ITEM:
            this.rightArm.xRot = this.rightArm.xRot * 0.5 F - ((float) Math.PI / 10 F);
            this.rightArm.yRot = 0.0 F;
            break;
        case THROW_SPEAR:
            this.rightArm.xRot = this.rightArm.xRot * 0.5 F - (p_102876_.isCrouching() ? 0.2617994 F : 0.0 F) - (float) Math.PI;
            this.rightArm.yRot = 0.0 F;
            break;
        case BOW_AND_ARROW:
            this.rightArm.yRot = -0.1 F + this.head.yRot;
            this.leftArm.yRot = 0.1 F + this.head.yRot + 0.4 F;
            this.rightArm.xRot = (-(float) Math.PI / 2 F) + this.head.xRot;
            this.leftArm.xRot = (-(float) Math.PI / 2 F) + this.head.xRot;
            break;
        case CROSSBOW_CHARGE:
            AnimationUtils.animateCrossbowCharge(this.rightArm, this.leftArm, p_102876_, true);
            break;
        case CROSSBOW_HOLD:
            AnimationUtils.animateCrossbowHold(this.rightArm, this.leftArm, this.head, true);
            break;
        case BRUSH:
            this.rightArm.xRot = this.rightArm.xRot * 0.5 F - ((float) Math.PI / 5 F);
            this.rightArm.yRot = 0.0 F;
            break;
        case SPYGLASS:
            this.rightArm.xRot = Mth.clamp(this.head.xRot - 1.9198622 F - (p_102876_.isCrouching() ? 0.2617994 F : 0.0 F), -2.4 F, 3.3 F);
            this.rightArm.yRot = this.head.yRot - 0.2617994 F;
            break;
        case TOOT_HORN:
            this.rightArm.xRot = Mth.clamp(this.head.xRot, -1.2 F, 1.2 F) - 1.4835298 F;
            this.rightArm.yRot = this.head.yRot - ((float) Math.PI / 6 F);
    }}

private void poseLeftArm(T p_102879_) {
    switch (this.leftArmPose) {
        case EMPTY:
            this.leftArm.yRot = 0.0 F;
            break;
        case BLOCK:
            this.leftArm.xRot = this.leftArm.xRot * 0.5 F - 0.9424779 F;
            this.leftArm.yRot = ((float) Math.PI / 6 F);
            break;
        case ITEM:
            this.leftArm.xRot = this.leftArm.xRot * 0.5 F - ((float) Math.PI / 10 F);
            this.leftArm.yRot = 0.0 F;
            break;
        case THROW_SPEAR:
            this.leftArm.xRot = this.leftArm.xRot * 0.5 F - (p_102879_.isCrouching() ? 0.2617994 F : 0.0 F) - (float) Math.PI;
            this.leftArm.yRot = 0.0 F;
            break;
        case BOW_AND_ARROW:
            this.rightArm.yRot = -0.1 F + this.head.yRot - 0.4 F;
            this.leftArm.yRot = 0.1 F + this.head.yRot;
            this.rightArm.xRot = (-(float) Math.PI / 2 F) + this.head.xRot;
            this.leftArm.xRot = (-(float) Math.PI / 2 F) + this.head.xRot;
            break;
        case CROSSBOW_CHARGE:
            AnimationUtils.animateCrossbowCharge(this.rightArm, this.leftArm, p_102879_, false);
            break;
        case CROSSBOW_HOLD:
            AnimationUtils.animateCrossbowHold(this.rightArm, this.leftArm, this.head, false);
            break;
        case BRUSH:
            this.leftArm.xRot = this.leftArm.xRot * 0.5 F - ((float) Math.PI / 5 F);
            this.leftArm.yRot = 0.0 F;
            break;
        case SPYGLASS:
            this.leftArm.xRot = Mth.clamp(this.head.xRot - 1.9198622 F - (p_102879_.isCrouching() ? 0.2617994 F : 0.0 F), -2.4 F, 3.3 F);
            this.leftArm.yRot = this.head.yRot + 0.2617994 F;
            break;
        case TOOT_HORN:
            this.leftArm.xRot = Mth.clamp(this.head.xRot, -1.2 F, 1.2 F) - 1.4835298 F;
            this.leftArm.yRot = this.head.yRot + ((float) Math.PI / 6 F);
    }
}

 

Fix:

No code analysis needed, a simple fix is to override the super method by using it's current name tag offset and then add 0.3F to the height. Then it'll correctly position the Warden's Nametag.

net.minecraft.world.entity.monster.warden.Warden.java // Updated

public class Warden extends Monster implements VibrationSystem {
    public float getNameTagOffsetY() {
        return super.getNameTagOffsetY() + 0.3F;
    }
}
[media]

Code Analysis:

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

net.minecraft.client.particle.DripParticle.java // Original

@OnlyIn(Dist.CLIENT)
public class DripParticle extends TextureSheetParticle {
    public static TextureSheetParticle createLavaHangParticle(SimpleParticleType p_273228_, ClientLevel p_273622_, double p_273666_, double p_273570_, double p_273214_, double p_273664_, double p_273595_, double p_272690_) {
        return new DripParticle.CoolingDripHangParticle(p_273622_, p_273666_, p_273570_, p_273214_, Fluids.LAVA, ParticleTypes.FALLING_LAVA);
    }
    public static TextureSheetParticle createLavaFallParticle(SimpleParticleType p_273238_, ClientLevel p_273752_, double p_272651_, double p_273625_, double p_273136_, double p_273204_, double p_272797_, double p_273362_) {
        DripParticle dripparticle = new DripParticle.FallAndLandParticle(p_273752_, p_272651_, p_273625_, p_273136_, Fluids.LAVA, ParticleTypes.LANDING_LAVA);
        dripparticle.setColor(1.0 F, 0.2857143 F, 0.083333336 F);
        return dripparticle;
    }
    public static TextureSheetParticle createLavaLandParticle(SimpleParticleType p_273607_, ClientLevel p_272692_, double p_273544_, double p_272768_, double p_272726_, double p_273719_, double p_272833_, double p_272949_) {
        DripParticle dripparticle = new DripParticle.DripLandParticle(p_272692_, p_273544_, p_272768_, p_272726_, Fluids.LAVA);
        dripparticle.setColor(1.0 F, 0.2857143 F, 0.083333336 F);
        return dripparticle;
    }
}

As seen in this code, we see that the lava particles are missing the attribute isGlowing. But if we take a look at the DripParticle#createObsidianTearHangParticle we see that sets the particle's isGlowing to true.

 

net.minecraft.client.particle.DripParticle.java // Original

public static TextureSheetParticle createObsidianTearHangParticle(SimpleParticleType p_273120_, ClientLevel p_272664_, double p_272879_, double p_272592_, double p_272967_, double p_272834_, double p_273440_, double p_272888_) {
     DripParticle.DripHangParticle dripparticle$driphangparticle = new DripParticle.DripHangParticle(p_272664_, p_272879_, p_272592_, p_272967_, Fluids.EMPTY, ParticleTypes.FALLING_OBSIDIAN_TEAR);
     dripparticle$driphangparticle.isGlowing = true;
     dripparticle$driphangparticle.gravity *= 0.01 F;
     dripparticle$driphangparticle.lifetime = 100;
     dripparticle$driphangparticle.setColor(0.51171875 F, 0.03125 F, 0.890625 F);
     return dripparticle$driphangparticle;
 }

Fix:

A simple fix to this is to set isGlowing to true on all 3 creations of the lava particle.

net.minecraft.client.particle.DripParticle.java // Updated

@OnlyIn(Dist.CLIENT)
public class DripParticle extends TextureSheetParticle {
    public static TextureSheetParticle createLavaHangParticle(SimpleParticleType p_273228_, ClientLevel p_273622_, double p_273666_, double p_273570_, double p_273214_, double p_273664_, double p_273595_, double p_272690) {
        DripParticle dripparticle = new DripParticle.CoolingDripHangParticle(p_273622_, p_273666_, p_273570_, p_273214_, Fluids.LAVA, ParticleTypes.FALLING_LAVA);
        dripparticle.isGlowing = true;
        return dripparticle;
    }
    public static TextureSheetParticle createLavaFallParticle(SimpleParticleType p_273238_, ClientLevel p_273752_, double p_272651_, double p_273625_, double p_273136_, double p_273204_, double p_272797_, double p_273362_) {
        DripParticle dripparticle = new DripParticle.FallAndLandParticle(p_273752_, p_272651_, p_273625_, p_273136_, Fluids.LAVA, ParticleTypes.LANDING_LAVA);
        dripparticle.isGlowing = true;
        dripparticle.setColor(1.0 F, 0.2857143 F, 0.083333336 F);
        return dripparticle;
    }
    public static TextureSheetParticle createLavaLandParticle(SimpleParticleType p_273607_, ClientLevel p_272692_, double p_273544_, double p_272768_, double p_272726_, double p_273719_, double p_272833_, double p_272949_) {
        DripParticle dripparticle = new DripParticle.DripLandParticle(p_272692_, p_273544_, p_272768_, p_272726_, Fluids.LAVA);
        dripparticle.isGlowing = true;
        dripparticle.setColor(1.0 F, 0.2857143 F, 0.083333336 F);
        return dripparticle;
    }
}
[media]

Code Analysis:

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

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

public abstract class Mob extends LivingEntity implements Targeting {
   ...
   protected void tickLeash() {
      if (this.leashInfoTag != null) {
         this.restoreLeashFromSave();
      }      if (this.leashHolder != null) {
         if (!this.isAlive() || !this.leashHolder.isAlive()) {
            this.dropLeash(true, true);
         }
      }
   }
   ...
}

As you can see above, there is no check to see if this#leashHolder is in spectator mode.

Fix:

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

public abstract class Mob extends LivingEntity implements Targeting {
   ...
   protected void tickLeash() {
      if (this.leashInfoTag != null) {
         this.restoreLeashFromSave();
      }      if (this.leashHolder != null) {
         if (!this.isAlive() || !this.leashHolder.isAlive() || this.leashHolder.isSpectator()) {
            this.dropLeash(true, true);
         }
      }
   }
   ...
}

An easy solution is to add a check if the leashHolder is in spectator. If so, then we should drop the leash. 

You're right, I completely forgot about optimizing the condition for any future ignitable items, the paper commit does fix the issue at hand. I've updated my comment to divert to the proper fix for the minecraft bug.

This issue being fixed could be quite revolutionary for map developers if they would like to play a song longer than the original disc's duration.
The following Code Analysis and Fix is just one of many solutions that Mojang could do to fix this situation!

Code Analysis:

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

net.minecraft.world.level.block.entity.JukeboxBlockEntity.java // Original

public class JukeboxBlockEntity extends BlockEntity implements Clearable, ContainerSingleItem {
    ...
    @VisibleForTesting
    public void startPlaying() {
        this.recordStartedTick = this.tickCount;
        this.isPlaying = true;
        this.level.updateNeighborsAt(this.getBlockPos(), this.getBlockState().getBlock());
        this.level.levelEvent((Player) null, 1010, this.getBlockPos(), Item.getId(this.getFirstItem().getItem()));
        this.setChanged();
    }
    ...
    public void setItem(int p_273461_, ItemStack p_273584_) {
        if (p_273584_.is(ItemTags.MUSIC_DISCS) && this.level != null) {
            this.items.set(p_273461_, p_273584_);
            this.setHasRecordBlockState((Entity) null, true);
            this.startPlaying();
        }
    }
    private boolean shouldRecordStopPlaying(RecordItem p_273267_) {
        return this.tickCount >= this.recordStartedTick + (long) p_273267_.getLengthInTicks() + 20 L;
    }
    ...
}

As we can see, there is no way to determine a custom length of a music disc, therefore the game stops it to the default record's duration. This can be seen at JukeboxBlockEntity#shouldRecordStopPlaying

Fix:

net.minecraft.world.level.block.entity.JukeboxBlockEntity.java // Updated

public class JukeboxBlockEntity extends BlockEntity implements Clearable, ContainerSingleItem {
    private long customDuration;
    ...
    @VisibleForTesting
    public void startPlaying(ItemStack itemStack) {
        this.customDuration = itemStack.getTag() != null ? itemStack.getTag().getLong("CustomDuration") : -1;
        this.recordStartedTick = this.tickCount;
        this.isPlaying = true;
        this.level.updateNeighborsAt(this.getBlockPos(), this.getBlockState().getBlock());
        this.level.levelEvent((Player) null, 1010, this.getBlockPos(), Item.getId(this.getFirstItem().getItem()));
        this.setChanged();
    }
    ...
    public void setItem(int p_273461_, ItemStack p_273584_) {
        if (p_273584_.is(ItemTags.MUSIC_DISCS) && this.level != null) {
            this.items.set(p_273461_, p_273584_);
            this.setHasRecordBlockState((Entity) null, true);
            this.startPlaying(p_273584_);
        }
    }
    private boolean shouldRecordStopPlaying(RecordItem p_273267_) {
        return this.tickCount >= this.recordStartedTick + ((this.customDuration == -1 ? (long) p_273267_.getLengthInTicks() + 20L : customDuration * 20L));
    }
    ...
}

With an ingenious fix I came up with, and the help with CompoundTags, this updated piece of code allows players to choose the CustomDuration of Music Discs, since there's no possible way to calculate the duration of music discs through resourcepacks, players instead can choose the duration of a music disc based on seconds.

For example, to give myself a music disc that will automatically stop playing after 5 seconds would look like:

/give @s minecraft:music_disc_cat{CustomDuration:5}

Upon playing this disc, it will automatically stop playing after 5 seconds.

This can be used to now, bypass the vanilla duration.

For example, to give myself a music disc that will automatically stop playing after 5 minutes.

/give @s minecraft:music_disc_cat{CustomDuration:300}

 

Cannot reproduce in 1.20.1, may I ask if you are reproducing this bug with the use of a spigot server, realm, or single player world?

The following code analysis below is inefficient for any future updates, please regard to the paper fix.

Code Analysis:

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

net.minecraft.world.entity.monster.Creeper.java // Original

public class Creeper extends Monster implements PowerableMob {
    ...
    protected InteractionResult mobInteract(Player p_32301_, InteractionHand p_32302_) {
        ItemStack itemstack = p_32301_.getItemInHand(p_32302_);
        if (itemstack.is(ItemTags.CREEPER_IGNITERS)) {
            SoundEvent soundevent = itemstack.is(Items.FIRE_CHARGE) ? SoundEvents.FIRECHARGE_USE : SoundEvents.FLINTANDSTEEL_USE;
            this.level().playSound(p_32301_, this.getX(), this.getY(), this.getZ(), soundevent, this.getSoundSource(), 1.0 F, this.random.nextFloat() * 0.4 F + 0.8 F);
            if (!this.level().isClientSide) {
                this.ignite();
                if (!itemstack.isDamageableItem()) {
                    itemstack.shrink(1);
                } else {
                    itemstack.hurtAndBreak(1, p_32301_, (p_32290_) - > {
                        p_32290_.broadcastBreakEvent(p_32302_);
                    });
                }
            }            return InteractionResult.sidedSuccess(this.level().isClientSide);
        } else {
            return super.mobInteract(p_32301_, p_32302_);
        }
    }
    ...
}

As reported by the OP, the ItemStack is being shrunken because ItemStack#isDamageableItem determines if it can be damaged (durability) or it is unbreakable. But, it never checks if the max itemstack is greater than one. Thus consuming the unbreakable flint and steel.  

Fix:

net.minecraft.world.entity.monster.Creeper.java // Updated

public class Creeper extends Monster implements PowerableMob {
    ...
    protected InteractionResult mobInteract(Player p_32301_, InteractionHand p_32302_) {
        ItemStack itemstack = p_32301_.getItemInHand(p_32302_);
        if (itemstack.is(ItemTags.CREEPER_IGNITERS)) {
            SoundEvent soundevent = itemstack.is(Items.FIRE_CHARGE) ? SoundEvents.FIRECHARGE_USE : SoundEvents.FLINTANDSTEEL_USE;
            this.level().playSound(p_32301_, this.getX(), this.getY(), this.getZ(), soundevent, this.getSoundSource(), 1.0 F, this.random.nextFloat() * 0.4 F + 0.8 F);
            if (!this.level().isClientSide) {
                this.ignite();
                if (!itemstack.isDamageableItem() && itemstack.getMaxStackSize() > 1) {
                    itemstack.shrink(1);
                } else {
                    itemstack.hurtAndBreak(1, p_32301_, (p_32290_) - > {
                        p_32290_.broadcastBreakEvent(p_32302_);
                    });
                }
            }            return InteractionResult.sidedSuccess(this.level().isClientSide);
        } else {
            return super.mobInteract(p_32301_, p_32302_);
        }
    }
    ...
}

This simple fix, checks if the itemstack used has a great stack size than 1, if so, then we should consume the itemstack used.

Code Analysis:

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

net.minecraft.client.Camera.java // Original

public class Camera {
    ...
    public FogType getFluidInCamera() {
            if (!this.initialized) {
                return FogType.NONE;
            } else {
                FluidState fluidstate = this.level.getFluidState(this.blockPosition);
                if (fluidstate.is(FluidTags.WATER) && this.position.y < (double)((float) this.blockPosition.getY() + fluidstate.getHeight(this.level, this.blockPosition))) {
                    return FogType.WATER;
                } else {
                    Camera.NearPlane camera$nearplane = this.getNearPlane();                    for (Vec3 vec3: Arrays.asList(camera$nearplane.forward, camera$nearplane.getTopLeft(), camera$nearplane.getTopRight(), camera$nearplane.getBottomLeft(), camera$nearplane.getBottomRight())) {
                        Vec3 vec31 = this.position.add(vec3);
                        BlockPos blockpos = BlockPos.containing(vec31);
                        FluidState fluidstate1 = this.level.getFluidState(blockpos);
                        if (fluidstate1.is(FluidTags.LAVA)) {
                            if (vec31.y <= (double)(fluidstate1.getHeight(this.level, blockpos) + (float) blockpos.getY())) {
                                return FogType.LAVA;
                            }
                        } else {
                            BlockState blockstate = this.level.getBlockState(blockpos);
                            if (blockstate.is(Blocks.POWDER_SNOW)) {
                                return FogType.POWDER_SNOW;
                            }
                        }
                    }                    return FogType.NONE;
                }
            }
        }
        ...
}

As we can see, this is the method that determines which fog to product to an entity's camera. As a result of this inspection, we can see that there is no conditions to determine whether a player is in spectator or not.

Fix:

net.minecraft.client.Camera.java // Updated

public class Camera {
    ...
    public FogType getFluidInCamera() {
            if (!this.initialized || (this.entity instanceof Player player) && player.isSpectator()) {
                return FogType.NONE;
            } else {
                FluidState fluidstate = this.level.getFluidState(this.blockPosition);
                if (fluidstate.is(FluidTags.WATER) && this.position.y < (double)((float) this.blockPosition.getY() + fluidstate.getHeight(this.level, this.blockPosition))) {
                    return FogType.WATER;
                } else {
                    Camera.NearPlane camera$nearplane = this.getNearPlane();                    for (Vec3 vec3: Arrays.asList(camera$nearplane.forward, camera$nearplane.getTopLeft(), camera$nearplane.getTopRight(), camera$nearplane.getBottomLeft(), camera$nearplane.getBottomRight())) {
                        Vec3 vec31 = this.position.add(vec3);
                        BlockPos blockpos = BlockPos.containing(vec31);
                        FluidState fluidstate1 = this.level.getFluidState(blockpos);
                        if (fluidstate1.is(FluidTags.LAVA)) {
                            if (vec31.y <= (double)(fluidstate1.getHeight(this.level, blockpos) + (float) blockpos.getY())) {
                                return FogType.LAVA;
                            }
                        } else {
                            BlockState blockstate = this.level.getBlockState(blockpos);
                            if (blockstate.is(Blocks.POWDER_SNOW)) {
                                return FogType.POWDER_SNOW;
                            }
                        }
                    }                    return FogType.NONE;
                }
            }
        }
        ...
}

This fix removes any sort of fog whenever the player is in spectator. Giving spectators free reign to look around at anything without any disturbance.

Code Analysis:

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

net.minecraft.client.Camera.java // Original

public class Camera {
    ...
    public FogType getFluidInCamera() {
            if (!this.initialized) {
                return FogType.NONE;
            } else {
                FluidState fluidstate = this.level.getFluidState(this.blockPosition);
                if (fluidstate.is(FluidTags.WATER) && this.position.y < (double)((float) this.blockPosition.getY() + fluidstate.getHeight(this.level, this.blockPosition))) {
                    return FogType.WATER;
                } else {
                    Camera.NearPlane camera$nearplane = this.getNearPlane();                    for (Vec3 vec3: Arrays.asList(camera$nearplane.forward, camera$nearplane.getTopLeft(), camera$nearplane.getTopRight(), camera$nearplane.getBottomLeft(), camera$nearplane.getBottomRight())) {
                        Vec3 vec31 = this.position.add(vec3);
                        BlockPos blockpos = BlockPos.containing(vec31);
                        FluidState fluidstate1 = this.level.getFluidState(blockpos);
                        if (fluidstate1.is(FluidTags.LAVA)) {
                            if (vec31.y <= (double)(fluidstate1.getHeight(this.level, blockpos) + (float) blockpos.getY())) {
                                return FogType.LAVA;
                            }
                        } else {
                            BlockState blockstate = this.level.getBlockState(blockpos);
                            if (blockstate.is(Blocks.POWDER_SNOW)) {
                                return FogType.POWDER_SNOW;
                            }
                        }
                    }                    return FogType.NONE;
                }
            }
        }
        ...
}

As we can see, this is the method that determines which fog to product to an entity's camera. As a result of this inspection, we can see that there is no conditions to determine whether a player is in spectator or not.

Fix:

net.minecraft.client.Camera.java // Updated

public class Camera {
    ...
    public FogType getFluidInCamera() {
            if (!this.initialized || (this.entity instanceof Player player) && player.isSpectator()) {
                return FogType.NONE;
            } else {
                FluidState fluidstate = this.level.getFluidState(this.blockPosition);
                if (fluidstate.is(FluidTags.WATER) && this.position.y < (double)((float) this.blockPosition.getY() + fluidstate.getHeight(this.level, this.blockPosition))) {
                    return FogType.WATER;
                } else {
                    Camera.NearPlane camera$nearplane = this.getNearPlane();                    for (Vec3 vec3: Arrays.asList(camera$nearplane.forward, camera$nearplane.getTopLeft(), camera$nearplane.getTopRight(), camera$nearplane.getBottomLeft(), camera$nearplane.getBottomRight())) {
                        Vec3 vec31 = this.position.add(vec3);
                        BlockPos blockpos = BlockPos.containing(vec31);
                        FluidState fluidstate1 = this.level.getFluidState(blockpos);
                        if (fluidstate1.is(FluidTags.LAVA)) {
                            if (vec31.y <= (double)(fluidstate1.getHeight(this.level, blockpos) + (float) blockpos.getY())) {
                                return FogType.LAVA;
                            }
                        } else {
                            BlockState blockstate = this.level.getBlockState(blockpos);
                            if (blockstate.is(Blocks.POWDER_SNOW)) {
                                return FogType.POWDER_SNOW;
                            }
                        }
                    }                    return FogType.NONE;
                }
            }
        }
        ...
}

This fix removes any sort of fog whenever the player is in spectator. Giving spectators free reign to look around at anything without any disturbance.

Code Analysis:

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

net.minecraft.client.MouseHandler.java // Original

public class MouseHandler {
    ...
    private void onPress(long p_91531_, int p_91532_, int p_91533_, int p_91534_) {
        if (p_91531_ == this.minecraft.getWindow().getWindow()) {
            if (this.minecraft.screen != null) {
                this.minecraft.setLastInputType(InputType.MOUSE);
            }
 
            boolean flag = p_91533_ == 1;
            if (Minecraft.ON_OSX && p_91532_ == 0) {
                if (flag) {
                    if ((p_91534_ & 2) == 2) {
                        p_91532_ = 1;
                        ++this.fakeRightMouse;
                    }
                } else if (this.fakeRightMouse > 0) {
                    p_91532_ = 1;
                    --this.fakeRightMouse;
                }
            }
 
            int i = p_91532_;
            if (flag) {
                if (this.minecraft.options.touchscreen().get() && this.clickDepth++ > 0) {
                    return;
                }
 
                this.activeButton = i;
                this.mousePressedTime = Blaze3D.getTime();
            } else if (this.activeButton != -1) {
                if (this.minecraft.options.touchscreen().get() && --this.clickDepth > 0) {
                    return;
                }                this.activeButton = -1;
            }
 
            boolean[] aboolean = new boolean[] {
                false
            };
             
            if (this.minecraft.getOverlay() == null) {
                if (this.minecraft.screen == null) {
                    if (!this.mouseGrabbed && flag) {
                        this.grabMouse();
                    }
                } else {
                    double d0 = this.xpos * (double) this.minecraft.getWindow().getGuiScaledWidth() / (double) this.minecraft.getWindow().getScreenWidth();
                    double d1 = this.ypos * (double) this.minecraft.getWindow().getGuiScaledHeight() / (double) this.minecraft.getWindow().getScreenHeight();
                    Screen screen = this.minecraft.screen;
                    if (flag) {
                        screen.afterMouseAction();
                        Screen.wrapScreenError(() - > {
                            aboolean[0] = screen.mouseClicked(d0, d1, i);
                        }, "mouseClicked event handler", screen.getClass().getCanonicalName());
                    } else {
                        Screen.wrapScreenError(() - > {
                            aboolean[0] = screen.mouseReleased(d0, d1, i);
                        }, "mouseReleased event handler", screen.getClass().getCanonicalName());
                    }
                }
            }
 
            if (!aboolean[0] && this.minecraft.screen == null && this.minecraft.getOverlay() == null) {
                if (i == 0) {
                    this.isLeftPressed = flag;
                } else if (i == 2) {
                    this.isMiddlePressed = flag;
                } else if (i == 1) {
                    this.isRightPressed = flag;
                }                KeyMapping.set(InputConstants.Type.MOUSE.getOrCreate(i), flag);
                if (flag) {
                    if (this.minecraft.player.isSpectator() && i == 2) {
                        this.minecraft.gui.getSpectatorGui().onMouseMiddleClick();
                    } else {
                        KeyMapping.click(InputConstants.Type.MOUSE.getOrCreate(i));
                    }
                }
            }
        }
    }
    ...
}

As we can see, we create a variable called flag, flag represents a boolean which is either true or false. I couldn't find the determination of what makes flag true, but my assumption is if the player has no external screen opened. If flag is false, then it will trigger the method Screen#wrapScreenError which will unpress the mouse keybind as it is setting aboolean[0] to true.

My solution is to remove that else statement, but there could be underlying bugs as I don't quite understand what that statement does. But that piece of code should be the root problem of why keybinds don't work with any mouse input since it's being occupied with an opened inventory.

Code Analysis:

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

net.minecraft.server.level.ServerPlayerGameMode.java // Original

public class ServerPlayerGameMode {
    ...
    public boolean destroyBlock(BlockPos p_9281_) {
       BlockState blockstate = this.level.getBlockState(p_9281_);
       if (!this.player.getMainHandItem().getItem().canAttackBlock(blockstate, this.level, p_9281_, this.player)) {
          return false;
       } else {
          BlockEntity blockentity = this.level.getBlockEntity(p_9281_);
          Block block = blockstate.getBlock();
          if (block instanceof GameMasterBlock && !this.player.canUseGameMasterBlocks()) {
             this.level.sendBlockUpdated(p_9281_, blockstate, blockstate, 3);
             return false;
          } else if (this.player.blockActionRestricted(this.level, p_9281_, this.gameModeForPlayer)) {
                    return false;
                } else {
                    block.playerWillDestroy(this.level, p_9281_, blockstate, this.player);
                    boolean flag = this.level.removeBlock(p_9281_, false);
                    if (flag) {
                        block.destroy(this.level, p_9281_, blockstate);
                    }                    

                    if (this.isCreative()) {
                        return true;
                    } else {
                        ItemStack itemstack = this.player.getMainHandItem();
                        ItemStack itemstack1 = itemstack.copy();
                        boolean flag1 = this.player.hasCorrectToolForDrops(blockstate);
                        itemstack.mineBlock(this.level, blockstate, p_9281_, this.player);
                        if (flag && flag1) {
                            block.playerDestroy(this.level, this.player, p_9281_, blockstate, blockentity, itemstack1);
                        }
    
                        return true;
                    }
                }
            }
        }
        ...
}

As you can see, there is a piece of code that checks if the player is in creative (this.isCreative()), if they are, then we return and do nothing about the block's state. Therefore this looks pretty intentional as a "feature" for Minecraft.
But, it looks like this bug report seems to be an official Mojang bug, so I will write a code analysis and fix to solve this problem.

Temporary Fix:

net.minecraft.server.level.ServerPlayerGameMode.java // Updated

public class ServerPlayerGameMode {
    ...
    public boolean destroyBlock(BlockPos p_9281_) {
            BlockState blockstate = this.level.getBlockState(p_9281_);
            if (!this.player.getMainHandItem().getItem().canAttackBlock(blockstate, this.level, p_9281_, this.player)) {
                return false;
            } else {
                BlockEntity blockentity = this.level.getBlockEntity(p_9281_);
                Block block = blockstate.getBlock();
                if (block instanceof GameMasterBlock && !this.player.canUseGameMasterBlocks()) {
                    this.level.sendBlockUpdated(p_9281_, blockstate, blockstate, 3);
                    return false;
                } else if (this.player.blockActionRestricted(this.level, p_9281_, this.gameModeForPlayer)) {
                    return false;
                } else {
                    block.playerWillDestroy(this.level, p_9281_, blockstate, this.player);
                    boolean flag = this.level.removeBlock(p_9281_, false);
                    if (flag) {
                        block.destroy(this.level, p_9281_, blockstate);
                    }                    

                    ItemStack itemstack = this.player.getMainHandItem();
                    ItemStack itemstack1 = itemstack.copy();
                    boolean flag1 = this.player.hasCorrectToolForDrops(blockstate);
                    itemstack.mineBlock(this.level, blockstate, p_9281_, this.player);
                    if (flag && flag1) {
                       block.playerDestroy(this.level, this.player, p_9281_, blockstate, blockentity, itemstack1);
                    }                        
                    
                    return true;
                }
            }
        }
        ...
}

In the code structure above, we simply remove the this.isCreative() check so logic of the block's state will update even in creative.

WARNING: This code analysis and fix will allow creative players to drop item(s) upon breaking the block.

Code Analysis:

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

net.minecraft.server.network.ServerGamePacketListenerImpl.java // Original

public class ServerGamePacketListenerImpl implements ServerPlayerConnection, TickablePacketListener, ServerGamePacketListener {
    ...
    public void handleUseItemOn(ServerboundUseItemOnPacket p_9930_) {
        PacketUtils.ensureRunningOnSameThread(p_9930_, this, this.player.serverLevel());
        this.player.connection.ackBlockChangesUpTo(p_9930_.getSequence());
        ServerLevel serverlevel = this.player.serverLevel();
        InteractionHand interactionhand = p_9930_.getHand();
        ItemStack itemstack = this.player.getItemInHand(interactionhand);
        if (itemstack.isItemEnabled(serverlevel.enabledFeatures())) {
            BlockHitResult blockhitresult = p_9930_.getHitResult();
            Vec3 vec3 = blockhitresult.getLocation();
            BlockPos blockpos = blockhitresult.getBlockPos();
            Vec3 vec31 = Vec3.atCenterOf(blockpos);
            if (!(this.player.getEyePosition().distanceToSqr(vec31) > MAX_INTERACTION_DISTANCE)) {
                Vec3 vec32 = vec3.subtract(vec31);
                double d0 = 1.0000001 D;
                if (Math.abs(vec32.x()) < 1.0000001 D && Math.abs(vec32.y()) < 1.0000001 D && Math.abs(vec32.z()) < 1.0000001 D) {
                    Direction direction = blockhitresult.getDirection();
                    this.player.resetLastActionTime();
                    int i = this.player.level().getMaxBuildHeight();
                    if (blockpos.getY() < i) {
                        if (this.awaitingPositionFromClient == null && this.player.distanceToSqr((double) blockpos.getX() + 0.5 D, (double) blockpos.getY() + 0.5 D, (double) blockpos.getZ() + 0.5 D) < 64.0 D && serverlevel.mayInteract(this.player, blockpos)) {
                            InteractionResult interactionresult = this.player.gameMode.useItemOn(this.player, serverlevel, itemstack, interactionhand, blockhitresult);
                            if (direction == Direction.UP && !interactionresult.consumesAction() && blockpos.getY() >= i - 1 && wasBlockPlacementAttempt(this.player, itemstack)) {
                                Component component = Component.translatable("build.tooHigh", i - 1).withStyle(ChatFormatting.RED);
                                this.player.sendSystemMessage(component, true);
                            } else if (interactionresult.shouldSwing()) {
                                this.player.swing(interactionhand, true);
                            }
                        }
                    } else {
                        Component component1 = Component.translatable("build.tooHigh", i - 1).withStyle(ChatFormatting.RED);
                        this.player.sendSystemMessage(component1, true);
                    }                    this.player.connection.send(new ClientboundBlockUpdatePacket(serverlevel, blockpos));
                    this.player.connection.send(new ClientboundBlockUpdatePacket(serverlevel, blockpos.relative(direction)));
                } else {
                    LOGGER.warn("Rejecting UseItemOnPacket from {}: Location {} too far away from hit block {}.", this.player.getGameProfile().getName(), vec3, blockpos);
                }
            }
        }
    }
    ...
}

As seen in the class ServerGamePacketListenerImpl, we see that there's only functionality for the max height warning indicator, not the min world height. 

Fix:

To fix this, all we have to do is to add compatibility for min world height. Really, it's just copy and pasting with a couple more modifications.

net.minecraft.server.network.ServerGamePacketListenerImpl.java // Updated

public class ServerGamePacketListenerImpl implements ServerPlayerConnection, TickablePacketListener, ServerGamePacketListener {
    ...
    public void handleUseItemOn(ServerboundUseItemOnPacket p_9930_) {
        PacketUtils.ensureRunningOnSameThread(p_9930_, this, this.player.serverLevel());
        this.player.connection.ackBlockChangesUpTo(p_9930_.getSequence());
        ServerLevel serverlevel = this.player.serverLevel();
        InteractionHand interactionhand = p_9930_.getHand();
        ItemStack itemstack = this.player.getItemInHand(interactionhand);
        if (itemstack.isItemEnabled(serverlevel.enabledFeatures())) {
            BlockHitResult blockhitresult = p_9930_.getHitResult();
            Vec3 vec3 = blockhitresult.getLocation();
            BlockPos blockpos = blockhitresult.getBlockPos();
            Vec3 vec31 = Vec3.atCenterOf(blockpos);
            if (!(this.player.getEyePosition().distanceToSqr(vec31) > MAX_INTERACTION_DISTANCE)) {
                Vec3 vec32 = vec3.subtract(vec31);
                double d0 = 1.0000001 D;
                if (Math.abs(vec32.x()) < 1.0000001 D && Math.abs(vec32.y()) < 1.0000001 D && Math.abs(vec32.z()) < 1.0000001 D) {
                    Direction direction = blockhitresult.getDirection();
                    this.player.resetLastActionTime();
                    int i = this.player.level().getMaxBuildHeight();
                    int j = this.player.level().getMinBuildHeight();                    if (blockpos.getY() < i && blockpos.getY() >= j) {
                        if (this.awaitingPositionFromClient == null && this.player.distanceToSqr((double) blockpos.getX() + 0.5 D, (double) blockpos.getY() + 0.5 D, (double) blockpos.getZ() + 0.5 D) < 64.0 D && serverlevel.mayInteract(this.player, blockpos)) {
                            InteractionResult interactionresult = this.player.gameMode.useItemOn(this.player, serverlevel, itemstack, interactionhand, blockhitresult);
                            if (direction == Direction.UP && !interactionresult.consumesAction() && blockpos.getY() >= i - 1 && wasBlockPlacementAttempt(this.player, itemstack)) {
                                Component component = Component.translatable("build.tooHigh", i - 1).withStyle(ChatFormatting.RED);
                                this.player.sendSystemMessage(component, true);
                            } else if (direction == Direction.DOWN && !interactionresult.consumesAction() && blockpos.getY() <= j && wasBlockPlacementAttempt(this.player, itemstack)) {
                                Component component = Component.translatable("build.tooLower", j + 1).withStyle(ChatFormatting.RED);
                                this.player.sendSystemMessage(component, true);
                            } else if (interactionresult.shouldSwing()) {
                                this.player.swing(interactionhand, true);
                            }
                        }
                    } else {
                        if (direction == Direction.UP) {
                            Component component1 = Component.translatable("build.tooHigh", i - 1).withStyle(ChatFormatting.RED);
                            this.player.sendSystemMessage(component1, true);
                        } else if (direction == Direction.DOWN) {
                            Component component1 = Component.translatable("build.tooLower", j + 1).withStyle(ChatFormatting.RED);
                            this.player.sendSystemMessage(component1, true);
                        }
                    }                    this.player.connection.send(new ClientboundBlockUpdatePacket(serverlevel, blockpos));
                    this.player.connection.send(new ClientboundBlockUpdatePacket(serverlevel, blockpos.relative(direction)));
                } else {
                    LOGGER.warn("Rejecting UseItemOnPacket from {}: Location {} too far away from hit block {}.", this.player.getGameProfile().getName(), vec3, blockpos);
                }
            }
        }
    }
    ...
}

A fair warning though, this may be a solution to indicate a lower build limit warning, you will need to add a translatable to en_us.json for the key: build.toLower.

As an example:
"build.tooLower": "Height limit for building is %s",

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;
            }
            ...
        }
        ...
    }    
    ...
    private boolean shouldStopFishing(Player p_37137_) {
        ItemStack itemstack = p_37137_.getMainHandItem();
        ItemStack itemstack1 = p_37137_.getOffhandItem();
        boolean flag = itemstack.is(Items.FISHING_ROD);
        boolean flag1 = itemstack1.is(Items.FISHING_ROD);
        if (!p_37137_.isRemoved() && p_37137_.isAlive() && (flag || flag1) && !(this.distanceToSqr(p_37137_) > 1024.0D)) {
            return false;
        } else {
            this.discard();
            return true;
        }
    }
    ...
}

As we can see in the code, FishingHook#shouldStopFishing is used to determine the distance from the player and the fishing rod. If the distance is too high, it will delete the fishing line, therefore no animations, sounds, and reward comes out of it.

Fix:

To fix this, we can simply remove !(this.distanceToSqr(p_37137) > 1024.0D)) from FishingHook#shouldStopFishing and move it to FishingHook#tick. Then, we can create our own additional "if" statement to check the distance and handle actually animating, rewarding, and emitting sound globally.

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

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.distanceToSqr(player) > 1024.0 D && player.fishing != null) {
            ItemStack itemStack = player.getMainHandItem().isEmpty() ? player.getOffhandItem() : player.getMainHandItem();
            InteractionHand interactionHand = player.getMainHandItem().isEmpty() ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
            int i = player.fishing.retrieve(itemStack);
            itemStack.hurtAndBreak(i, player, (p_41288_) -> p_41288_.broadcastBreakEvent(interactionHand));
            player.swing(interactionHand);
            player.level().playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.FISHING_BOBBER_RETRIEVE, SoundSource.NEUTRAL, 1.0 F, 0.4 F / (player.level().getRandom().nextFloat() * 0.4 F + 0.8 F));
        } else if (this.level().isClientSide || !this.shouldStopFishing(player)) {
            if (this.onGround()) {
                ++this.life;
                if (this.life >= 1200) {
                    this.discard();
                    return;
                }
            } else {
                this.life = 0;
            }
            ...
        }
        ...
    }
    ...
    private boolean shouldStopFishing(Player p_37137_) {
        ItemStack itemstack = p_37137_.getMainHandItem();
        ItemStack itemstack1 = p_37137_.getOffhandItem();
        boolean flag = itemstack.is(Items.FISHING_ROD);
        boolean flag1 = itemstack1.is(Items.FISHING_ROD);
        if (!p_37137_.isRemoved() && p_37137_.isAlive() && (flag || flag1)) {
            return false;
        } else {
            this.discard();
            return true;
        }
    }
    ...
}

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!

Code Analysis:

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

net.minecraft.client.renderer.entity.FishingHookRenderer.java // Original

public class FishingHookRenderer extends EntityRenderer<FishingHook> {
   ...
   int i = player.getMainArm() == HumanoidArm.RIGHT ? 1 : -1; 
   ItemStack itemstack = player.getMainHandItem(); 
   if (!itemstack.is(Items.FISHING_ROD)) { 
      i = -i; 
   }
   ...
}

We can take a look at this specific part of code in the decompiled version of Minecraft 1.20.1, as we can see, if a fishing rod is NOT present in the player's main arm, as a default it will be the right hand. Then it will automatically move the fishing line towards the player's offhand, without it even checking for a fishing rod in the offhand.

Fix:

To fix this, we can easily add a condition to the current "if" statement in the code above, checking if the player has a fishing rod in the offhand. Therefore, whenever you swap out of the fishing rod, it'll persist on the default side of the mainhand.

net.minecraft.client.renderer.entity.FishingHookRenderer.java // Updated

public class FishingHookRenderer extends EntityRenderer<FishingHook> {
   ...
   int i = player.getMainArm() == HumanoidArm.RIGHT ? 1 : -1;
   ItemStack itemstack = player.getMainHandItem();
   if (!itemstack.is(Items.FISHING_ROD) && player.getOffhandItem().is(Items.FISHING_ROD)) {
      i = -i;
   }
   ...
}