|
17 | 17 | import meteordevelopment.meteorclient.utils.player.InvUtils; |
18 | 18 | import meteordevelopment.meteorclient.utils.world.TickRate; |
19 | 19 | import meteordevelopment.orbit.EventHandler; |
| 20 | +import net.minecraft.world.InteractionHand; |
20 | 21 | import net.minecraft.world.entity.projectile.FishingHook; |
21 | 22 | import net.minecraft.world.item.FishingRodItem; |
22 | 23 | import net.minecraft.world.item.ItemStack; |
23 | 24 | import net.minecraft.world.item.enchantment.Enchantments; |
| 25 | +import org.jspecify.annotations.NullMarked; |
| 26 | +import org.jspecify.annotations.Nullable; |
24 | 27 |
|
| 28 | +@NullMarked |
25 | 29 | public class AutoFish extends Module { |
26 | 30 | private final SettingGroup sgGeneral = settings.getDefaultGroup(); |
27 | 31 |
|
@@ -82,107 +86,173 @@ public class AutoFish extends Module { |
82 | 86 | .build() |
83 | 87 | ); |
84 | 88 |
|
85 | | - public AutoFish() { |
86 | | - super(Categories.Player, "auto-fish", "Automatically fishes for you."); |
| 89 | + private enum State { |
| 90 | + IDLE, |
| 91 | + WAITING_FOR_BITE, |
| 92 | + WAITING_TO_REEL |
87 | 93 | } |
88 | 94 |
|
| 95 | + private State state = State.IDLE; |
89 | 96 | private double castDelayLeft = 0.0; |
90 | 97 | private double catchDelayLeft = 0.0; |
91 | | - private boolean wasHooked = false; |
| 98 | + |
| 99 | + public AutoFish() { |
| 100 | + super(Categories.Player, "auto-fish", "Automatically fishes for you."); |
| 101 | + } |
92 | 102 |
|
93 | 103 | @Override |
94 | 104 | public void onActivate() { |
| 105 | + state = State.IDLE; |
95 | 106 | castDelayLeft = 0.0; |
96 | 107 | catchDelayLeft = 0.0; |
| 108 | + } |
97 | 109 |
|
98 | | - wasHooked = false; |
| 110 | + @Override |
| 111 | + public void onDeactivate() { |
| 112 | + state = State.IDLE; |
99 | 113 | } |
100 | 114 |
|
101 | 115 | @EventHandler |
102 | 116 | private void onTick(TickEvent.Pre event) { |
103 | | - int bestRodSlot = findBestRod(); |
104 | | - |
105 | | - if (autoSwitch.get() && bestRodSlot != -1 && mc.player.getInventory().getSelectedSlot() != bestRodSlot) { |
106 | | - InvUtils.swap(bestRodSlot, false); |
107 | | - } |
108 | | - |
109 | | - if (!(mc.player.getMainHandItem().getItem() instanceof FishingRodItem)) return; |
110 | | - |
111 | | - tryCast(); |
112 | | - tryCatch(); |
| 117 | + if (mc.player.fishing != null) handleFishing(); |
| 118 | + else handleIdle(); |
113 | 119 | } |
114 | 120 |
|
115 | | - private void tryCast() { |
116 | | - if (mc.player.fishing != null) return; |
| 121 | + private void handleIdle() { |
| 122 | + if (state != State.IDLE) state = State.IDLE; |
117 | 123 |
|
| 124 | + RodCandidate candidate = findBestRodCandidate(); |
| 125 | + if (candidate == null) return; |
| 126 | + if (!prepareRod(candidate)) return; |
118 | 127 | if (!autoCast.get()) return; |
119 | 128 |
|
120 | 129 | if (castDelayLeft > 0) { |
121 | 130 | castDelayLeft -= TickRate.INSTANCE.getTickRate() / 20.0; |
122 | 131 | return; |
123 | 132 | } |
124 | 133 |
|
125 | | - useRod(); |
| 134 | + cast(); |
126 | 135 | } |
127 | 136 |
|
128 | | - private void tryCatch() { |
129 | | - if (mc.player.fishing == null) return; |
130 | | - if (mc.player.fishing.getHookedIn() != null) { |
131 | | - useRod(); |
132 | | - return; |
| 137 | + private void handleFishing() { |
| 138 | + switch (state) { |
| 139 | + case IDLE -> state = State.WAITING_FOR_BITE; |
| 140 | + case WAITING_FOR_BITE -> { |
| 141 | + if (mc.player.fishing.getHookedIn() != null) { |
| 142 | + reel(); |
| 143 | + return; |
| 144 | + } |
| 145 | + |
| 146 | + if (mc.player.fishing.currentState != FishingHook.FishHookState.BOBBING) return; |
| 147 | + if (((FishingHookAccessor) mc.player.fishing).meteor$hasCaughtFish()) { |
| 148 | + catchDelayLeft = randomizeDelay(catchDelay.get(), catchDelayVariance.get()); |
| 149 | + state = State.WAITING_TO_REEL; |
| 150 | + } |
| 151 | + } |
| 152 | + case WAITING_TO_REEL -> { |
| 153 | + if (mc.player.fishing.getHookedIn() != null) { |
| 154 | + reel(); |
| 155 | + return; |
| 156 | + } |
| 157 | + |
| 158 | + if (catchDelayLeft > 0) { |
| 159 | + catchDelayLeft -= TickRate.INSTANCE.getTickRate() / 20.0; |
| 160 | + return; |
| 161 | + } |
| 162 | + |
| 163 | + reel(); |
| 164 | + } |
133 | 165 | } |
| 166 | + } |
134 | 167 |
|
135 | | - if (mc.player.fishing.currentState != FishingHook.FishHookState.BOBBING) return; |
| 168 | + private void cast() { |
| 169 | + if (useRod()) state = State.WAITING_FOR_BITE; |
| 170 | + } |
136 | 171 |
|
137 | | - if (!wasHooked) { |
138 | | - if (((FishingHookAccessor) mc.player.fishing).meteor$hasCaughtFish()) { |
139 | | - catchDelayLeft = randomizeDelay(catchDelay.get(), catchDelayVariance.get()); |
140 | | - wasHooked = true; |
141 | | - } |
| 172 | + private void reel() { |
| 173 | + if (useRod()) state = State.IDLE; |
| 174 | + } |
142 | 175 |
|
143 | | - return; |
144 | | - } |
| 176 | + private boolean useRod() { |
| 177 | + RodCandidate candidate = findBestRodCandidate(); |
| 178 | + if (candidate == null) return false; |
| 179 | + if (!prepareRod(candidate)) return false; |
145 | 180 |
|
146 | | - if (catchDelayLeft > 0) { |
147 | | - catchDelayLeft -= TickRate.INSTANCE.getTickRate() / 20.0; |
148 | | - return; |
149 | | - } |
| 181 | + mc.gameMode.useItem(mc.player, candidate.hand()); |
| 182 | + castDelayLeft = randomizeDelay(castDelay.get(), castDelayVariance.get()); |
| 183 | + return true; |
| 184 | + } |
150 | 185 |
|
151 | | - useRod(); |
| 186 | + @SuppressWarnings("BooleanMethodIsAlwaysInverted") |
| 187 | + private boolean prepareRod(RodCandidate candidate) { |
| 188 | + if (candidate.hand() == InteractionHand.OFF_HAND) return true; |
| 189 | + if (candidate.hotbarSlot() == mc.player.getInventory().getSelectedSlot()) return true; |
| 190 | + if (!autoSwitch.get()) return false; |
| 191 | + |
| 192 | + InvUtils.swap(candidate.hotbarSlot(), false); |
| 193 | + return candidate.hotbarSlot() == mc.player.getInventory().getSelectedSlot(); |
152 | 194 | } |
153 | 195 |
|
154 | | - private void useRod() { |
155 | | - Utils.rightClick(); |
156 | | - wasHooked = false; |
157 | | - castDelayLeft = randomizeDelay(castDelay.get(), castDelayVariance.get()); |
| 196 | + private boolean isUsableRod(ItemStack stack) { |
| 197 | + if (!(stack.getItem() instanceof FishingRodItem)) return false; |
| 198 | + return !antiBreak.get() || stack.getDamageValue() != stack.getMaxDamage() - 1; |
158 | 199 | } |
159 | 200 |
|
160 | | - private int findBestRod() { |
161 | | - int bestSlot = -1; |
162 | | - int bestScore = -1; |
| 201 | + private int scoreRod(ItemStack stack) { |
| 202 | + int score = 0; |
| 203 | + |
| 204 | + score += Utils.getEnchantmentLevel(stack, Enchantments.LUCK_OF_THE_SEA); |
| 205 | + score += Utils.getEnchantmentLevel(stack, Enchantments.LURE); |
| 206 | + score += Utils.getEnchantmentLevel(stack, Enchantments.MENDING); |
| 207 | + score += Utils.getEnchantmentLevel(stack, Enchantments.UNBREAKING); |
| 208 | + |
| 209 | + return score; |
| 210 | + } |
| 211 | + |
| 212 | + /// Finds the best rod candidate in the player's inventory and offhand. |
| 213 | + /// |
| 214 | + /// Tie-break order: |
| 215 | + /// 1. Selected hotbar slot |
| 216 | + /// 2. Remaining hotbar slots in stable 0..8 order |
| 217 | + /// 3. Offhand |
| 218 | + /// |
| 219 | + /// Candidates only replace the current best when they have a strictly higher score, |
| 220 | + /// preserving the ordering above for equal scores. |
| 221 | + private @Nullable RodCandidate findBestRodCandidate() { |
| 222 | + int selectedSlot = mc.player.getInventory().getSelectedSlot(); |
| 223 | + RodCandidate best = null; |
| 224 | + |
| 225 | + ItemStack mainHandStack = mc.player.getMainHandItem(); |
| 226 | + if (isUsableRod(mainHandStack)) { |
| 227 | + best = new RodCandidate(InteractionHand.MAIN_HAND, selectedSlot, scoreRod(mainHandStack)); |
| 228 | + } |
163 | 229 |
|
164 | 230 | for (int i = 0; i < 9; i++) { |
165 | | - ItemStack stack = mc.player.getInventory().getItem(i); |
166 | | - if (!(stack.getItem() instanceof FishingRodItem)) continue; |
167 | | - if (antiBreak.get() && stack.getDamageValue() == stack.getMaxDamage() - 1) continue; |
| 231 | + if (i == selectedSlot) continue; |
168 | 232 |
|
169 | | - int score = 0; |
| 233 | + ItemStack stack = mc.player.getInventory().getItem(i); |
| 234 | + if (!isUsableRod(stack)) continue; |
170 | 235 |
|
171 | | - score += Utils.getEnchantmentLevel(stack, Enchantments.LUCK_OF_THE_SEA); |
172 | | - score += Utils.getEnchantmentLevel(stack, Enchantments.LURE); |
173 | | - score += Utils.getEnchantmentLevel(stack, Enchantments.MENDING); |
174 | | - score += Utils.getEnchantmentLevel(stack, Enchantments.UNBREAKING); |
| 236 | + int score = scoreRod(stack); |
175 | 237 |
|
176 | | - if (score > bestScore) { |
177 | | - bestScore = score; |
178 | | - bestSlot = i; |
| 238 | + if (best == null || score > best.score()) { |
| 239 | + best = new RodCandidate(InteractionHand.MAIN_HAND, i, score); |
179 | 240 | } |
| 241 | + } |
180 | 242 |
|
181 | | - // Found a maxed out rod |
182 | | - if (score == 10) break; |
| 243 | + ItemStack offhandStack = mc.player.getOffhandItem(); |
| 244 | + if (isUsableRod(offhandStack)) { |
| 245 | + int score = scoreRod(offhandStack); |
| 246 | + |
| 247 | + if (best == null || score > best.score()) { |
| 248 | + best = new RodCandidate(InteractionHand.OFF_HAND, -1, score); |
| 249 | + } |
183 | 250 | } |
184 | 251 |
|
185 | | - return bestSlot; |
| 252 | + return best; |
| 253 | + } |
| 254 | + |
| 255 | + private record RodCandidate(InteractionHand hand, int hotbarSlot, int score) { |
186 | 256 | } |
187 | 257 |
|
188 | 258 | private double randomizeDelay(int delay, int variance) { |
|
0 commit comments