diff options
| author | Peyton Brown <81496880+PeytonBrown@users.noreply.github.com> | 2025-07-14 00:36:47 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-14 00:36:47 -0400 |
| commit | c3177618bb8a08d0b3f26858858477509ecdb8ba (patch) | |
| tree | 1bd47ce6352e8a12191ccd4c007335ec279c04b6 /src/main/java | |
| parent | 6e1deb0bc3d3fbb8c5768e116762de3f6770f953 (diff) | |
| download | Skyblocker-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')
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; + } + } |
