aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorPeyton Brown <81496880+PeytonBrown@users.noreply.github.com>2025-07-14 00:36:47 -0400
committerGitHub <noreply@github.com>2025-07-14 00:36:47 -0400
commitc3177618bb8a08d0b3f26858858477509ecdb8ba (patch)
tree1bd47ce6352e8a12191ccd4c007335ec279c04b6 /src/main/java
parent6e1deb0bc3d3fbb8c5768e116762de3f6770f953 (diff)
downloadSkyblocker-c3177618bb8a08d0b3f26858858477509ecdb8ba.tar.gz
Skyblocker-c3177618bb8a08d0b3f26858858477509ecdb8ba.tar.bz2
Skyblocker-c3177618bb8a08d0b3f26858858477509ecdb8ba.zip
Tuner solver (#1430)
* init * Add slot text * handle slot interaction * Cleanup and improve solvers * Add setting for tuner solver * fix merge conflict * Change to use container solver. * Add button to ContainerSolver::onClickSlot * fix merge conflicts, reformat using tabs * reformat using tabs * handle looping clicks * remove unused import * Fix merge conflicts
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/ForagingCategory.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/ForagingConfig.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/bazaar/ReorderHelper.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/ColorTerminal.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/LightsOnTerminal.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/OrderTerminal.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/SameColorTerminal.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/StartsWithTerminal.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/experiment/ChronomatronSolver.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/experiment/SuperpairsSolver.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/experiment/UltrasequencerSolver.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/galatea/TunerSolver.java525
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/container/ContainerSolver.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/container/ContainerSolverManager.java6
17 files changed, 558 insertions, 17 deletions
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/ForagingCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/ForagingCategory.java
index 61b99cb7..74246e2e 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/ForagingCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/ForagingCategory.java
@@ -71,6 +71,14 @@ public class ForagingCategory {
})
.controller(IntegerController.createBuilder().range(1, 4).slider(1).build())
.build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.foraging.galatea.enableTunerSolver"))
+ .description(Text.translatable("skyblocker.config.foraging.galatea.enableTunerSolver.@Tooltip"))
+ .binding(defaults.foraging.galatea.enableTunerSolver,
+ () -> config.foraging.galatea.enableTunerSolver,
+ newValue -> config.foraging.galatea.enableTunerSolver = newValue)
+ .controller(ConfigUtils.createBooleanController())
+ .build())
.build())
//Sweep Overlays
.group(OptionGroup.createBuilder()
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/ForagingConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/ForagingConfig.java
index a67ea866..f2d0be6f 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/ForagingConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/ForagingConfig.java
@@ -18,6 +18,8 @@ public class ForagingConfig {
public boolean enableSeaLumiesHighlighter = true;
public int seaLumiesMinimumCount = 3;
+
+ public boolean enableTunerSolver = true;
}
public static class SweepOverlay {
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
index 44df0f33..0679920b 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
@@ -25,6 +25,7 @@ import de.hysky.skyblocker.skyblock.fishing.FishingHelper;
import de.hysky.skyblocker.skyblock.fishing.FishingHookDisplayHelper;
import de.hysky.skyblocker.skyblock.fishing.SeaCreatureTracker;
import de.hysky.skyblocker.skyblock.galatea.ForestNodes;
+import de.hysky.skyblocker.skyblock.galatea.TunerSolver;
import de.hysky.skyblocker.skyblock.slayers.SlayerManager;
import de.hysky.skyblocker.skyblock.slayers.boss.demonlord.FirePillarAnnouncer;
import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListManager;
@@ -152,6 +153,7 @@ public abstract class ClientPlayNetworkHandlerMixin extends ClientCommonNetworkH
@Inject(method = "onPlaySound", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/NetworkThreadUtils;forceMainThread(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/listener/PacketListener;Lnet/minecraft/util/thread/ThreadExecutor;)V", shift = At.Shift.AFTER), cancellable = true)
private void skyblocker$onPlaySound(PlaySoundS2CPacket packet, CallbackInfo ci) {
CrystalsChestHighlighter.onSound(packet);
+ TunerSolver.INSTANCE.onSound(packet);
SoundEvent sound = packet.getSound().value();
// Mute Enderman sounds in the End
diff --git a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
index becad8c0..935ebec6 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
@@ -319,7 +319,7 @@ public abstract class HandledScreenMixin<T extends ScreenHandler> extends Screen
}
if (currentSolver != null) {
- boolean disallowed = ContainerSolverManager.onSlotClick(slotId, stack);
+ boolean disallowed = ContainerSolverManager.onSlotClick(slotId, stack, button);
if (disallowed) ci.cancel();
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/bazaar/ReorderHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/bazaar/ReorderHelper.java
index 1394df07..0aa49f18 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/bazaar/ReorderHelper.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/bazaar/ReorderHelper.java
@@ -33,7 +33,7 @@ public class ReorderHelper extends SimpleContainerSolver implements TooltipAdder
}
@Override
- public boolean onClickSlot(int slot, ItemStack stack, int screenId) {
+ public boolean onClickSlot(int slot, ItemStack stack, int screenId, int button) {
// V This part is so that it short-circuits if not necessary
if ((slot == 11 || slot == 13) && stack.isOf(Items.GREEN_TERRACOTTA) && InputUtil.isKeyPressed(MinecraftClient.getInstance().getWindow().getHandle(), GLFW.GLFW_KEY_LEFT_CONTROL)) {
Matcher matcher;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/ColorTerminal.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/ColorTerminal.java
index dce4ddcc..27982981 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/ColorTerminal.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/ColorTerminal.java
@@ -55,7 +55,7 @@ public final class ColorTerminal extends SimpleContainerSolver implements Termin
}
@Override
- public boolean onClickSlot(int slot, ItemStack stack, int screenId) {
+ public boolean onClickSlot(int slot, ItemStack stack, int screenId, int button) {
if (stack.hasGlint() || !targetColor.equals(itemColor.get(stack.getItem()))) {
return shouldBlockIncorrectClicks();
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/LightsOnTerminal.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/LightsOnTerminal.java
index f4617953..2dfb22f2 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/LightsOnTerminal.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/LightsOnTerminal.java
@@ -31,7 +31,7 @@ public final class LightsOnTerminal extends SimpleContainerSolver implements Ter
}
@Override
- public boolean onClickSlot(int slot, ItemStack stack, int screenId) {
+ public boolean onClickSlot(int slot, ItemStack stack, int screenId, int button) {
return stack.isOf(Items.LIME_STAINED_GLASS_PANE);
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/OrderTerminal.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/OrderTerminal.java
index 254c5f7a..18251850 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/OrderTerminal.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/OrderTerminal.java
@@ -57,7 +57,7 @@ public final class OrderTerminal extends SimpleContainerSolver implements Termin
}
@Override
- public boolean onClickSlot(int slot, ItemStack stack, int screenId) {
+ public boolean onClickSlot(int slot, ItemStack stack, int screenId, int button) {
if (stack == null || stack.isEmpty()) return false;
if (!stack.isOf(Items.RED_STAINED_GLASS_PANE) || stack.getCount() != currentNum + 1) {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/SameColorTerminal.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/SameColorTerminal.java
index 8bf01ab7..c98f13ed 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/SameColorTerminal.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/SameColorTerminal.java
@@ -95,7 +95,7 @@ public final class SameColorTerminal extends SimpleContainerSolver implements Te
}
@Override
- public boolean onClickSlot(int slot, ItemStack stack, int screenId) {
+ public boolean onClickSlot(int slot, ItemStack stack, int screenId, int button) {
if (clickMap.containsKey(slot) && clickMap.get(slot) == 0) {
return shouldBlockIncorrectClicks();
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/StartsWithTerminal.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/StartsWithTerminal.java
index c03f6ba6..58ec4546 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/StartsWithTerminal.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/StartsWithTerminal.java
@@ -51,7 +51,7 @@ public final class StartsWithTerminal extends SimpleContainerSolver implements T
}
@Override
- public boolean onClickSlot(int slot, ItemStack stack, int screenId) {
+ public boolean onClickSlot(int slot, ItemStack stack, int screenId, int button) {
//Some random glass pane was clicked or something
if (!trackedItemStates.containsKey(slot) || stack == null || stack.isEmpty()) return false;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/experiment/ChronomatronSolver.java b/src/main/java/de/hysky/skyblocker/skyblock/experiment/ChronomatronSolver.java
index cb2b0a2e..62ff2c53 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/experiment/ChronomatronSolver.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/experiment/ChronomatronSolver.java
@@ -125,7 +125,7 @@ public final class ChronomatronSolver extends ExperimentSolver {
* Increments {@link #chronomatronCurrentOrdinal} if the item clicked matches the item at {@link #chronomatronCurrentOrdinal the current index} in the chain.
*/
@Override
- public boolean onClickSlot(int slot, ItemStack stack, int screenId) {
+ public boolean onClickSlot(int slot, ItemStack stack, int screenId, int button) {
if (getState() == State.SHOW) {
Item item = chronomatronSlots.get(chronomatronCurrentOrdinal);
if ((stack.isOf(item) || ChronomatronSolver.TERRACOTTA_TO_GLASS.get(stack.getItem()) == item)) {
@@ -136,7 +136,7 @@ public final class ChronomatronSolver extends ExperimentSolver {
return shouldBlockIncorrectClicks();
}
}
- return super.onClickSlot(slot, stack, screenId);
+ return super.onClickSlot(slot, stack, screenId, button);
}
@Override
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/experiment/SuperpairsSolver.java b/src/main/java/de/hysky/skyblocker/skyblock/experiment/SuperpairsSolver.java
index c6a22e0f..b3c92a06 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/experiment/SuperpairsSolver.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/experiment/SuperpairsSolver.java
@@ -97,12 +97,12 @@ public final class SuperpairsSolver extends ExperimentSolver {
}
@Override
- public boolean onClickSlot(int slot, ItemStack stack, int screenId) {
+ public boolean onClickSlot(int slot, ItemStack stack, int screenId, int button) {
if (getState() == State.SHOW) {
this.superpairsPrevClickedSlot = slot;
this.superpairsCurrentSlot = ItemStack.EMPTY;
}
- return super.onClickSlot(slot, stack, screenId);
+ return super.onClickSlot(slot, stack, screenId, button);
}
@Override
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/experiment/UltrasequencerSolver.java b/src/main/java/de/hysky/skyblocker/skyblock/experiment/UltrasequencerSolver.java
index a142ffab..624c44d1 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/experiment/UltrasequencerSolver.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/experiment/UltrasequencerSolver.java
@@ -22,7 +22,7 @@ public final class UltrasequencerSolver extends ExperimentSolver {
public static final UltrasequencerSolver INSTANCE = new UltrasequencerSolver();
/**
* The playable slots of Ultrasequencer in the Metaphysical level.
- *
+ * <p>
* Even though the Supreme/Transcendent levels have less playable slots we filter out black glass panes later on
* since black isn't in the color sequence.
*/
@@ -107,7 +107,7 @@ public final class UltrasequencerSolver extends ExperimentSolver {
*/
@SuppressWarnings("JavadocReference")
@Override
- public boolean onClickSlot(int slot, ItemStack stack, int screenId) {
+ public boolean onClickSlot(int slot, ItemStack stack, int screenId, int button) {
if (getState() == State.SHOW) {
if (slot == ultrasequencerNextSlot) {
int count = getSlots().get(ultrasequencerNextSlot).getCount() + 1;
@@ -120,7 +120,7 @@ public final class UltrasequencerSolver extends ExperimentSolver {
return shouldBlockIncorrectClicks();
}
}
- return super.onClickSlot(slot, stack, screenId);
+ return super.onClickSlot(slot, stack, screenId, button);
}
/**
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/galatea/TunerSolver.java b/src/main/java/de/hysky/skyblocker/skyblock/galatea/TunerSolver.java
new file mode 100644
index 00000000..6dc81792
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/galatea/TunerSolver.java
@@ -0,0 +1,525 @@
+package de.hysky.skyblocker.skyblock.galatea;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.container.SimpleContainerSolver;
+import de.hysky.skyblocker.utils.container.SlotTextAdder;
+import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket;
+import net.minecraft.screen.GenericContainerScreenHandler;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.text.Text;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class TunerSolver extends SimpleContainerSolver implements SlotTextAdder {
+ private static final Logger LOGGER = LoggerFactory.getLogger(TunerSolver.class);
+
+ public static final TunerSolver INSTANCE = new TunerSolver();
+
+ private TunerSolver() {
+ super("^Tune Frequency$");
+ }
+
+ private static final Item[] COLOR_CYCLE = {
+ Items.MAGENTA_DYE, Items.LIGHT_BLUE_DYE, Items.YELLOW_DYE, Items.LIME_DYE,
+ Items.PINK_DYE, Items.CYAN_DYE, Items.PURPLE_DYE, Items.LAPIS_LAZULI,
+ Items.COCOA_BEANS, Items.GREEN_DYE, Items.RED_DYE, Items.BONE_MEAL,
+ Items.ORANGE_DYE
+ };
+
+ private static final Item[] GLASS_CYCLE = {
+ Items.MAGENTA_STAINED_GLASS_PANE, Items.LIGHT_BLUE_STAINED_GLASS_PANE,
+ Items.YELLOW_STAINED_GLASS_PANE, Items.LIME_STAINED_GLASS_PANE,
+ Items.PINK_STAINED_GLASS_PANE, Items.CYAN_STAINED_GLASS_PANE,
+ Items.PURPLE_STAINED_GLASS_PANE, Items.BLUE_STAINED_GLASS_PANE,
+ Items.BROWN_STAINED_GLASS_PANE, Items.GREEN_STAINED_GLASS_PANE,
+ Items.RED_STAINED_GLASS_PANE, Items.WHITE_STAINED_GLASS_PANE,
+ Items.ORANGE_STAINED_GLASS_PANE
+ };
+
+ private static final String[] PITCH_CYCLE = {"Low", "Normal", "High"};
+ private static final float[] PITCH_VALUES = {0.0952381f, 0.7936508f, 1.4920635f};
+
+ private static final int[] SPEED_CYCLE = {1, 2, 3, 4, 5};
+ private static final int[][] SPEED_RANGES = {
+ {50, 64}, // Speed 1
+ {40, 49}, // Speed 2
+ {30, 39}, // Speed 3
+ {20, 29}, // Speed 4
+ {10, 19} // Speed 5
+ };
+
+ // Solver results
+ private int colorClicks = 0;
+ private int speedClicks = 0;
+ private int pitchClicks = 0;
+
+ private boolean colorSolved = false;
+ private boolean speedSolved = false;
+ private boolean pitchSolved = false;
+
+ // Flag to ensure getRequiredClicks runs only once per screen
+ private boolean hasProcessed = false;
+ private boolean isInMenu = false;
+
+ // Pitch tracking
+ private String currentPitch = null;
+ private final List<Float> recentPitches = new ArrayList<>();
+ private static final int MAX_PITCH_SAMPLES = 5;
+
+ // Target pane movement tracking
+ private int lastTargetSlot = -1;
+ private int ticksSinceLastMove = 0;
+ private int targetSpeed = -1; // Latest target speed from tick interval
+ private int lastSpeedTicks = 0;
+
+ @Override
+ public boolean isEnabled() {
+ return SkyblockerConfigManager.get().foraging.galatea.enableTunerSolver;
+ }
+
+ @Override
+ public List<ColorHighlight> getColors(Int2ObjectMap<ItemStack> slots) {
+ if (!hasProcessed) {
+ ItemStack dyeStack = slots.get(46);
+ if (dyeStack != null && !dyeStack.isEmpty() && isDye(dyeStack.getItem())) {
+ if (!colorSolved) {
+ colorClicks = computeColorClicks(slots);
+ colorSolved = true;
+ }
+ if (!speedSolved) {
+ maybeSolveSpeed(slots);
+ }
+ if (!pitchSolved) {
+ currentPitch = readCurrentPitch(slots);
+ }
+ hasProcessed = true;
+ }
+ }
+ return List.of();
+ }
+
+ @Override
+ public void start(GenericContainerScreen screen) {
+ resetState();
+ isInMenu = true;
+ ScreenEvents.afterTick(screen).register(s -> {
+ Int2ObjectMap<ItemStack> slots = getSlots(screen);
+ trackTargetPaneMovement(slots);
+ });
+ ScreenEvents.remove(screen).register(s -> resetState());
+ }
+
+ @Override
+ public void reset() {
+ resetState();
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(@Nullable Slot slot, @NotNull ItemStack stack, int slotId) {
+ if (!isEnabled()) {
+ return List.of();
+ }
+ if (slotId == 46 && colorSolved) {
+ return SlotText.bottomRightList(Text.literal(String.valueOf(colorClicks)).withColor(SlotText.LIGHT_GREEN));
+ }
+ if (slotId == 48 && speedSolved) {
+ return SlotText.bottomRightList(Text.literal(String.valueOf(speedClicks)).withColor(SlotText.LIGHT_GREEN));
+ }
+ if (slotId == 50 && pitchSolved) {
+ return SlotText.bottomRightList(Text.literal(String.valueOf(pitchClicks)).withColor(SlotText.LIGHT_GREEN));
+ }
+ return List.of();
+ }
+
+ /**
+ * Updates the remaining click counters when the corresponding tuner slot
+ * is clicked. The counters are adjusted based on the cycle length of the
+ * element to correctly handle wrapping when clicking through the cycle
+ * multiple times.
+ */
+ @Override
+ public boolean onClickSlot(int slotId, ItemStack stack, int screenId, int button) {
+ if (!SkyblockerConfigManager.get().foraging.galatea.enableTunerSolver) return false;
+ if (!isInMenu) return false;
+
+ if (button != 0 && button != 1) return false;
+
+ int delta = button == 0 ? -1 : 1;
+
+ if (colorSolved && slotId == 46) {
+ colorClicks = updateClicks(colorClicks, COLOR_CYCLE.length, delta);
+ } else if (speedSolved && slotId == 48) {
+ speedClicks = updateClicks(speedClicks, SPEED_CYCLE.length, delta);
+ } else if (pitchSolved && slotId == 50) {
+ pitchClicks = updateClicks(pitchClicks, PITCH_CYCLE.length, delta);
+ }
+ return false;
+ }
+
+ /**
+ * Adjusts the remaining clicks taking into account the cycle length so that
+ * looping through the values keeps the counter accurate.
+ *
+ * @param clicks current remaining clicks
+ * @param cycleLength length of the cycle (number of options)
+ * @param delta change in clicks; {@code -1} for decrement, {@code 1} for increment
+ * @return the updated click count
+ */
+ private static int updateClicks(int clicks, int cycleLength, int delta) {
+ int forward = clicks >= 0 ? clicks : cycleLength + clicks; // distance when moving forward
+ forward = (forward + delta + cycleLength) % cycleLength;
+ int backward = cycleLength - forward;
+ return forward <= backward ? forward : -backward;
+ }
+
+
+ private void resetState() {
+ hasProcessed = false;
+ isInMenu = false;
+ colorSolved = false;
+ speedSolved = false;
+ pitchSolved = false;
+ colorClicks = 0;
+ speedClicks = 0;
+ pitchClicks = 0;
+ currentPitch = null;
+ recentPitches.clear();
+ lastTargetSlot = -1;
+ ticksSinceLastMove = 0;
+ targetSpeed = -1;
+ lastSpeedTicks = 0;
+ }
+
+ private void trackTargetPaneMovement(Int2ObjectMap<ItemStack> slots) {
+ int currentTargetSlot = -1;
+
+ // Find the current target pane in slots 10–16
+ for (int slot = 10; slot <= 16; slot++) {
+ ItemStack stack = slots.get(slot);
+ if (stack != null && isStainedGlassPane(stack.getItem())) {
+ currentTargetSlot = slot;
+ break;
+ }
+ }
+
+ // Check if the target pane slot has changed
+ if (currentTargetSlot != lastTargetSlot && lastTargetSlot != -1) {
+
+ // Calculate target speed from tick interval
+ int ticks = ticksSinceLastMove;
+ targetSpeed = -1;
+ for (int i = 0; i < SPEED_RANGES.length; i++) {
+ if (ticks >= SPEED_RANGES[i][0] && ticks <= SPEED_RANGES[i][1]) {
+ targetSpeed = SPEED_CYCLE[i];
+ break;
+ }
+ }
+ if (targetSpeed == -1) {
+ LOGGER.warn("Tick interval {} does not match any speed range", ticks);
+ }
+ lastSpeedTicks = ticks;
+
+ ticksSinceLastMove = 0;
+
+ if (!speedSolved) {
+ maybeSolveSpeed(slots);
+ }
+ } else if (currentTargetSlot != -1) {
+ ticksSinceLastMove++;
+ }
+
+ lastTargetSlot = currentTargetSlot;
+ }
+
+ /**
+ * Determines the number of clicks needed to match the dye color in slot 46
+ * to the target glass pane color in slots 10–16.
+ *
+ * @param slots map of slot indices to their {@link ItemStack}
+ * @return number of clicks for color (+ for forward, - for backward, 0 if invalid)
+ */
+ private static int computeColorClicks(Int2ObjectMap<ItemStack> slots) {
+
+ // Read dye in slot 46
+ ItemStack dyeStack = slots.get(46);
+ if (dyeStack == null || dyeStack.isEmpty()) {
+ LOGGER.warn("No dye found in slot 46");
+ return 0;
+ }
+ Item dyeItem = dyeStack.getItem();
+ int dyeIndex = getColorIndex(dyeItem, COLOR_CYCLE);
+ if (dyeIndex == -1) {
+ LOGGER.warn("Invalid dye item in slot 46: {}", dyeItem);
+ return 0;
+ }
+
+ // Find the moving glass pane in slots 28–34
+ ItemStack movingPane = null;
+ int movingSlot = -1;
+ for (int slot = 28; slot <= 34; slot++) {
+ ItemStack stack = slots.get(slot);
+ if (stack != null && isStainedGlassPane(stack.getItem())) {
+ movingPane = stack;
+ movingSlot = slot;
+ break;
+ }
+ }
+ if (movingPane == null) {
+ LOGGER.warn("No stained glass pane found in slots 28–34");
+ return 0;
+ }
+ Item movingItem = movingPane.getItem();
+ int movingIndex = getColorIndex(movingItem, GLASS_CYCLE);
+ if (movingIndex == -1) {
+ LOGGER.warn("Invalid glass pane item in slot {}: {}", movingSlot, movingItem);
+ return 0;
+ }
+
+ // Find the target glass pane in slots 10–16
+ ItemStack targetPane = null;
+ int targetSlot = -1;
+ for (int slot = 10; slot <= 16; slot++) {
+ ItemStack stack = slots.get(slot);
+ if (stack != null && isStainedGlassPane(stack.getItem())) {
+ targetPane = stack;
+ targetSlot = slot;
+ break;
+ }
+ }
+ if (targetPane == null) {
+ LOGGER.warn("No stained glass pane found in slots 10–16");
+ return 0;
+ }
+ Item targetItem = targetPane.getItem();
+ int targetIndex = getColorIndex(targetItem, GLASS_CYCLE);
+ if (targetIndex == -1) {
+ LOGGER.warn("Invalid glass pane item in slot {}: {}", targetSlot, targetItem);
+ return 0;
+ }
+
+ // Calculate clicks to match dye to target pane
+ int clicks = calculateClicks(dyeIndex, targetIndex);
+ LOGGER.info("Color solved: Dye={}, Target={}, Required clicks={}",
+ dyeStack.getName().getString(),
+ targetPane.getName().getString(),
+ clicks >= 0 ? "+" + clicks : clicks);
+ return clicks;
+ }
+
+ public void onSound(PlaySoundS2CPacket packet) {
+ if (!SkyblockerConfigManager.get().foraging.galatea.enableTunerSolver
+ || pitchSolved || !Utils.isInGalatea() || !isInMenu
+ || !packet.getSound().value().id().equals(SoundEvents.BLOCK_NOTE_BLOCK_BASS.value().id())) {
+ return;
+ }
+
+ float packetPitch = packet.getPitch();
+ recentPitches.add(packetPitch);
+ int sampleCount = recentPitches.size();
+ String name = getPitchName(packetPitch);
+
+ if (currentPitch == null) {
+ LOGGER.warn("Current pitch not set, cannot compare");
+ recentPitches.clear();
+ return;
+ }
+
+ float expectedPitch = getPitchValue(currentPitch);
+ if (Math.abs(packetPitch - expectedPitch) > 0.0001f) {
+ String targetPitch = name;
+ if (targetPitch == null) {
+ LOGGER.warn("Invalid pitch value received: {}", packetPitch);
+ recentPitches.clear();
+ return;
+ }
+
+ int currentIndex = getPitchIndex(currentPitch);
+ int targetIndex = getPitchIndex(targetPitch);
+ if (currentIndex == -1 || targetIndex == -1) {
+ LOGGER.warn("Invalid pitch indices: current={}, target={}", currentPitch, targetPitch);
+ recentPitches.clear();
+ return;
+ }
+
+ int clicks = calculatePitchClicks(currentIndex, targetIndex);
+ LOGGER.info("Pitch solved: Current={}, Target={}, Required clicks={}",
+ currentPitch, targetPitch, clicks >= 0 ? "+" + clicks : clicks);
+ pitchClicks = clicks;
+ pitchSolved = true;
+ recentPitches.clear();
+ return;
+ }
+
+ if (sampleCount >= MAX_PITCH_SAMPLES) {
+ pitchClicks = 0;
+ pitchSolved = true;
+ LOGGER.info("Pitch solved: Current={}, Target={}, Required clicks=+0 (all samples match)",
+ currentPitch,
+ currentPitch);
+ recentPitches.clear();
+ }
+ }
+
+ private static int readCurrentSpeed(Int2ObjectMap<ItemStack> slots) {
+ ItemStack speedStack = slots.get(48);
+ if (speedStack != null && !speedStack.isEmpty()) {
+ List<Text> lore = ItemUtils.getLore(speedStack);
+ if (lore.size() >= 4) {
+ try {
+ String speedText = lore.get(3).getString();
+ String[] parts = speedText.split(": ");
+ int currentSpeed = Integer.parseInt(parts[1].trim());
+ if (currentSpeed >= 1 && currentSpeed <= 5) {
+ return currentSpeed;
+ }
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ }
+ return 0;
+ }
+
+ private static String readCurrentPitch(Int2ObjectMap<ItemStack> slots) {
+ ItemStack pitchStack = slots.get(50);
+ if (pitchStack != null && !pitchStack.isEmpty()) {
+ List<Text> lore = ItemUtils.getLore(pitchStack);
+ if (lore.size() >= 3) {
+ String pitchText = lore.get(2).getString();
+ if (pitchText.contains("Low")) return "Low";
+ if (pitchText.contains("Normal")) return "Normal";
+ if (pitchText.contains("High")) return "High";
+ }
+ }
+ return null;
+ }
+
+ private void maybeSolveSpeed(Int2ObjectMap<ItemStack> slots) {
+ int currentSpeed = readCurrentSpeed(slots);
+ if (currentSpeed > 0 && targetSpeed != -1) {
+ int currentIndex = getSpeedIndex(currentSpeed);
+ int targetIndex = getSpeedIndex(targetSpeed);
+ if (currentIndex != -1 && targetIndex != -1) {
+ speedClicks = calculateSpeedClicks(currentIndex, targetIndex);
+ speedSolved = true;
+ LOGGER.info(
+ "Speed solved: Current={}, Target={}, Ticks={}, Required clicks={}",
+ currentSpeed,
+ targetSpeed,
+ lastSpeedTicks,
+ speedClicks >= 0 ? "+" + speedClicks : speedClicks);
+ } else {
+ LOGGER.warn("Invalid speed indices: current={}, target={}", currentSpeed, targetSpeed);
+ }
+ }
+ }
+
+ private static int getSpeedIndex(int speed) {
+ for (int i = 0; i < SPEED_CYCLE.length; i++) {
+ if (SPEED_CYCLE[i] == speed) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private static int calculateSpeedClicks(int fromIndex, int toIndex) {
+ int forward = (toIndex - fromIndex + SPEED_CYCLE.length) % SPEED_CYCLE.length;
+ int backward = (fromIndex - toIndex + SPEED_CYCLE.length) % SPEED_CYCLE.length;
+ return forward <= backward ? forward : -backward;
+ }
+
+ private static float getPitchValue(String pitch) {
+ for (int i = 0; i < PITCH_CYCLE.length; i++) {
+ if (PITCH_CYCLE[i].equals(pitch)) {
+ return PITCH_VALUES[i];
+ }
+ }
+ return 0f;
+ }
+
+ private static String getPitchName(float pitch) {
+ for (int i = 0; i < PITCH_VALUES.length; i++) {
+ if (Math.abs(pitch - PITCH_VALUES[i]) < 0.0001f) {
+ return PITCH_CYCLE[i];
+ }
+ }
+ return null;
+ }
+
+ private static int getPitchIndex(String pitch) {
+ for (int i = 0; i < PITCH_CYCLE.length; i++) {
+ if (PITCH_CYCLE[i].equals(pitch)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private static int calculatePitchClicks(int fromIndex, int toIndex) {
+ int forward = (toIndex - fromIndex + PITCH_CYCLE.length) % PITCH_CYCLE.length;
+ int backward = (fromIndex - toIndex + PITCH_CYCLE.length) % PITCH_CYCLE.length;
+ return forward <= backward ? forward : -backward;
+ }
+
+ private static boolean isDye(Item item) {
+ for (Item dye : COLOR_CYCLE) {
+ if (item == dye) {
+ return true;
+ }
+ }