diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/player/AutoFish.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/player/AutoFish.java index 81dc56f8dc9..020adfeebf3 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/modules/player/AutoFish.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/player/AutoFish.java @@ -17,11 +17,15 @@ import meteordevelopment.meteorclient.utils.player.InvUtils; import meteordevelopment.meteorclient.utils.world.TickRate; import meteordevelopment.orbit.EventHandler; +import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.projectile.FishingHook; import net.minecraft.world.item.FishingRodItem; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.enchantment.Enchantments; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +@NullMarked public class AutoFish extends Module { private final SettingGroup sgGeneral = settings.getDefaultGroup(); @@ -82,39 +86,44 @@ public class AutoFish extends Module { .build() ); - public AutoFish() { - super(Categories.Player, "auto-fish", "Automatically fishes for you."); + private enum State { + IDLE, + WAITING_FOR_BITE, + WAITING_TO_REEL } + private State state = State.IDLE; private double castDelayLeft = 0.0; private double catchDelayLeft = 0.0; - private boolean wasHooked = false; + + public AutoFish() { + super(Categories.Player, "auto-fish", "Automatically fishes for you."); + } @Override public void onActivate() { + state = State.IDLE; castDelayLeft = 0.0; catchDelayLeft = 0.0; + } - wasHooked = false; + @Override + public void onDeactivate() { + state = State.IDLE; } @EventHandler private void onTick(TickEvent.Pre event) { - int bestRodSlot = findBestRod(); - - if (autoSwitch.get() && bestRodSlot != -1 && mc.player.getInventory().getSelectedSlot() != bestRodSlot) { - InvUtils.swap(bestRodSlot, false); - } - - if (!(mc.player.getMainHandItem().getItem() instanceof FishingRodItem)) return; - - tryCast(); - tryCatch(); + if (mc.player.fishing != null) handleFishing(); + else handleIdle(); } - private void tryCast() { - if (mc.player.fishing != null) return; + private void handleIdle() { + if (state != State.IDLE) state = State.IDLE; + RodCandidate candidate = findBestRodCandidate(); + if (candidate == null) return; + if (!prepareRod(candidate)) return; if (!autoCast.get()) return; if (castDelayLeft > 0) { @@ -122,67 +131,128 @@ private void tryCast() { return; } - useRod(); + cast(); } - private void tryCatch() { - if (mc.player.fishing == null) return; - if (mc.player.fishing.getHookedIn() != null) { - useRod(); - return; + private void handleFishing() { + switch (state) { + case IDLE -> state = State.WAITING_FOR_BITE; + case WAITING_FOR_BITE -> { + if (mc.player.fishing.getHookedIn() != null) { + reel(); + return; + } + + if (mc.player.fishing.currentState != FishingHook.FishHookState.BOBBING) return; + if (((FishingHookAccessor) mc.player.fishing).meteor$hasCaughtFish()) { + catchDelayLeft = randomizeDelay(catchDelay.get(), catchDelayVariance.get()); + state = State.WAITING_TO_REEL; + } + } + case WAITING_TO_REEL -> { + if (mc.player.fishing.getHookedIn() != null) { + reel(); + return; + } + + if (catchDelayLeft > 0) { + catchDelayLeft -= TickRate.INSTANCE.getTickRate() / 20.0; + return; + } + + reel(); + } } + } - if (mc.player.fishing.currentState != FishingHook.FishHookState.BOBBING) return; + private void cast() { + if (useRod()) state = State.WAITING_FOR_BITE; + } - if (!wasHooked) { - if (((FishingHookAccessor) mc.player.fishing).meteor$hasCaughtFish()) { - catchDelayLeft = randomizeDelay(catchDelay.get(), catchDelayVariance.get()); - wasHooked = true; - } + private void reel() { + if (useRod()) state = State.IDLE; + } - return; - } + private boolean useRod() { + RodCandidate candidate = findBestRodCandidate(); + if (candidate == null) return false; + if (!prepareRod(candidate)) return false; - if (catchDelayLeft > 0) { - catchDelayLeft -= TickRate.INSTANCE.getTickRate() / 20.0; - return; - } + mc.gameMode.useItem(mc.player, candidate.hand()); + castDelayLeft = randomizeDelay(castDelay.get(), castDelayVariance.get()); + return true; + } - useRod(); + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean prepareRod(RodCandidate candidate) { + if (candidate.hand() == InteractionHand.OFF_HAND) return true; + if (candidate.hotbarSlot() == mc.player.getInventory().getSelectedSlot()) return true; + if (!autoSwitch.get()) return false; + + InvUtils.swap(candidate.hotbarSlot(), false); + return candidate.hotbarSlot() == mc.player.getInventory().getSelectedSlot(); } - private void useRod() { - Utils.rightClick(); - wasHooked = false; - castDelayLeft = randomizeDelay(castDelay.get(), castDelayVariance.get()); + private boolean isUsableRod(ItemStack stack) { + if (!(stack.getItem() instanceof FishingRodItem)) return false; + return !antiBreak.get() || stack.getDamageValue() != stack.getMaxDamage() - 1; } - private int findBestRod() { - int bestSlot = -1; - int bestScore = -1; + private int scoreRod(ItemStack stack) { + int score = 0; + + score += Utils.getEnchantmentLevel(stack, Enchantments.LUCK_OF_THE_SEA); + score += Utils.getEnchantmentLevel(stack, Enchantments.LURE); + score += Utils.getEnchantmentLevel(stack, Enchantments.MENDING); + score += Utils.getEnchantmentLevel(stack, Enchantments.UNBREAKING); + + return score; + } + + /// Finds the best rod candidate in the player's inventory and offhand. + /// + /// Tie-break order: + /// 1. Selected hotbar slot + /// 2. Remaining hotbar slots in stable 0..8 order + /// 3. Offhand + /// + /// Candidates only replace the current best when they have a strictly higher score, + /// preserving the ordering above for equal scores. + private @Nullable RodCandidate findBestRodCandidate() { + int selectedSlot = mc.player.getInventory().getSelectedSlot(); + RodCandidate best = null; + + ItemStack mainHandStack = mc.player.getMainHandItem(); + if (isUsableRod(mainHandStack)) { + best = new RodCandidate(InteractionHand.MAIN_HAND, selectedSlot, scoreRod(mainHandStack)); + } for (int i = 0; i < 9; i++) { - ItemStack stack = mc.player.getInventory().getItem(i); - if (!(stack.getItem() instanceof FishingRodItem)) continue; - if (antiBreak.get() && stack.getDamageValue() == stack.getMaxDamage() - 1) continue; + if (i == selectedSlot) continue; - int score = 0; + ItemStack stack = mc.player.getInventory().getItem(i); + if (!isUsableRod(stack)) continue; - score += Utils.getEnchantmentLevel(stack, Enchantments.LUCK_OF_THE_SEA); - score += Utils.getEnchantmentLevel(stack, Enchantments.LURE); - score += Utils.getEnchantmentLevel(stack, Enchantments.MENDING); - score += Utils.getEnchantmentLevel(stack, Enchantments.UNBREAKING); + int score = scoreRod(stack); - if (score > bestScore) { - bestScore = score; - bestSlot = i; + if (best == null || score > best.score()) { + best = new RodCandidate(InteractionHand.MAIN_HAND, i, score); } + } - // Found a maxed out rod - if (score == 10) break; + ItemStack offhandStack = mc.player.getOffhandItem(); + if (isUsableRod(offhandStack)) { + int score = scoreRod(offhandStack); + + if (best == null || score > best.score()) { + best = new RodCandidate(InteractionHand.OFF_HAND, -1, score); + } } - return bestSlot; + return best; + } + + private record RodCandidate(InteractionHand hand, int hotbarSlot, int score) { } private double randomizeDelay(int delay, int variance) {