diff --git a/src/main/java/net/runelite/client/plugins/microbot/geflipper/FlipperPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/geflipper/FlipperPlugin.java index 859239b5f7..cd74ac1bf2 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/geflipper/FlipperPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/geflipper/FlipperPlugin.java @@ -35,7 +35,7 @@ isExternal = PluginConstants.IS_EXTERNAL ) public class FlipperPlugin extends Plugin { - public static final String version = "1.2.80"; + public static final String version = "1.2.81"; private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FlipperPlugin.class); @Inject private Client client; diff --git a/src/main/java/net/runelite/client/plugins/microbot/geflipper/FlipperScript.java b/src/main/java/net/runelite/client/plugins/microbot/geflipper/FlipperScript.java index 937a01622c..c61a008a32 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/geflipper/FlipperScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/geflipper/FlipperScript.java @@ -1482,12 +1482,16 @@ private boolean isSlotDefaultAction(int slotId, SlotActionExecutor.Action action return readUi(() -> { if (Microbot.naturalMouse == null || Microbot.targetMenu != null || Microbot.getClient().isMenuOpen() || !isSlotActionSwapEnabled() || !sameSlotSuggestion(key) || isOfferScreenOpen()) return false; - net.runelite.api.Point mouse = Microbot.getClient().getMouseCanvasPosition(); Widget slot = Microbot.getClient().getWidget(slotId); Widget button = slot == null ? null : slot.getChild(2); - if (button == null || button.isHidden() || mouse == null - || mouse.getX() != point.getX() || mouse.getY() != point.getY() - || !button.getBounds().contains(point.getX(), point.getY())) return false; + if (button == null || button.isHidden()) return false; + Rectangle bounds = button.getBounds(); + // Owned gestures can update PointerState a frame before the client mouse position. + net.runelite.api.Point mouse = Microbot.getClient().getMouseCanvasPosition(); + if (!SlotActionExecutor.cursorOnSlotButton(mouse, point, bounds) + && !SlotActionExecutor.cursorOnSlotButton(PointerState.get(), point, bounds)) { + return false; + } return SlotActionExecutor.matchesDefaultAction( Microbot.getClient().getMenu().getMenuEntries(), slotId, action); }); @@ -1520,47 +1524,53 @@ private boolean checkAndAbortOrModifyIfNeeded() { final int slotId = grandExchangeSlotIds[boxId]; final SlotActionExecutor.Action action = abort ? SlotActionExecutor.Action.ABORT : SlotActionExecutor.Action.MODIFY; - SlotActionExecutor.Result result = SlotActionExecutor.execute(config.slotAction(), action, slotId, - new SlotActionExecutor.Ui() { - private BooleanSupplier hoverGuard = () -> true; - private boolean variedHover; - public boolean slotSwapEnabled() { return isSlotActionSwapEnabled(); } - public net.runelite.api.Point actionPoint(int id, SlotActionExecutor.Action a) { - return slotActionPoint(id, a); - } - public boolean hover(net.runelite.api.Point point) { - // Mouse.move dispatches a single jump. Follow a smooth path on this - // script thread; never block the client thread for mouse movement. - if (randomizeMouseSpeed()) { - variedHover = true; - hoverGuard = mouseMovementGuard(); - return mouseMotion.move(point, true, hoverGuard); - } - if (Microbot.naturalMouse != null && !Thread.currentThread().isInterrupted()) { - Microbot.naturalMouse.moveTo(point.getX(), point.getY()); - } - return true; - } - public boolean awaitDefaultAction(int id, SlotActionExecutor.Action a, net.runelite.api.Point point) { - return hoverGuard.getAsBoolean() - && waitForUi(() -> !hoverGuard.getAsBoolean() || isSlotDefaultAction(id, a, point, key), 1800) - && hoverGuard.getAsBoolean(); - } - public boolean clickDefaultAction(int id, SlotActionExecutor.Action a, net.runelite.api.Point point) { - if (!hoverGuard.getAsBoolean() || !FlipperScript.this.isRunning() || Thread.currentThread().isInterrupted() - || !isSlotDefaultAction(id, a, point, key)) return false; - // Reuse the verified point; a rectangle would choose a different point. - return variedHover ? mouseMotion.click(point, true, hoverGuard) : clickTradingMouse(point); + SlotActionExecutor.Ui ui = new SlotActionExecutor.Ui() { + private BooleanSupplier hoverGuard = () -> true; + private boolean variedHover; + public boolean slotSwapEnabled() { return isSlotActionSwapEnabled(); } + public net.runelite.api.Point actionPoint(int id, SlotActionExecutor.Action a) { + return slotActionPoint(id, a); + } + public boolean hover(net.runelite.api.Point point) { + // Mouse.move dispatches a single jump. Follow a smooth path on this + // script thread; never block the client thread for mouse movement. + if (randomizeMouseSpeed()) { + variedHover = true; + hoverGuard = mouseMovementGuard(); + return mouseMotion.move(point, true, hoverGuard); } - public boolean invokeAction(int id, SlotActionExecutor.Action a, net.runelite.api.Point point) { - if (Thread.currentThread().isInterrupted() || !sameSlotSuggestion(key) - || isOfferScreenOpen() || slotActionPoint(id, a) == null) return false; - return invokeTradingMouse(new NewMenuEntry().option(a.option).target("") - .identifier(a.identifier).type(MenuAction.CC_OP).param0(2).param1(id) - .itemId(-1).forceLeftClick(false), - new Rectangle(point.getX() - 1, point.getY() - 1, 2, 2)); + if (Microbot.naturalMouse != null && !Thread.currentThread().isInterrupted()) { + Microbot.naturalMouse.moveTo(point.getX(), point.getY()); } - }); + return true; + } + public boolean awaitDefaultAction(int id, SlotActionExecutor.Action a, net.runelite.api.Point point) { + return hoverGuard.getAsBoolean() + && waitForUi(() -> !hoverGuard.getAsBoolean() || isSlotDefaultAction(id, a, point, key), 1800) + && hoverGuard.getAsBoolean(); + } + public boolean clickDefaultAction(int id, SlotActionExecutor.Action a, net.runelite.api.Point point) { + if (!hoverGuard.getAsBoolean() || !FlipperScript.this.isRunning() || Thread.currentThread().isInterrupted() + || !isSlotDefaultAction(id, a, point, key)) return false; + // Reuse the verified point; a rectangle would choose a different point. + return variedHover ? mouseMotion.click(point, true, hoverGuard) : clickTradingMouse(point); + } + public boolean invokeAction(int id, SlotActionExecutor.Action a, net.runelite.api.Point point) { + if (Thread.currentThread().isInterrupted() || !sameSlotSuggestion(key) + || isOfferScreenOpen() || slotActionPoint(id, a) == null) return false; + return invokeTradingMouse(new NewMenuEntry().option(a.option).target("") + .identifier(a.identifier).type(MenuAction.CC_OP).param0(2).param1(id) + .itemId(-1).forceLeftClick(false), + new Rectangle(point.getX() - 1, point.getY() - 1, 2, 2)); + } + }; + SlotActionExecutor.Result result = SlotActionExecutor.execute(config.slotAction(), action, slotId, ui); + FlipperConfig.SlotAction fallback = slotActionFallback(config.slotAction(), result); + if (fallback != null) { + log.info("Copilot left-click for {} was not ready after hover; using explicit slot menu action.", + action.option); + result = SlotActionExecutor.execute(fallback, action, slotId, ui); + } lastActionTime = System.currentTimeMillis(); actionCooldown = DEFAULT_ACTION_COOLDOWN; if (handleSlotActionFailure(result, action, key)) return true; @@ -1583,6 +1593,17 @@ && sameSlotSuggestion(key)) { return true; } } + /** + * After a hovered left-click never becomes ready, run the explicit slot menu action once. + * Returns null when no fallback should run. + */ + static FlipperConfig.SlotAction slotActionFallback(FlipperConfig.SlotAction mode, + SlotActionExecutor.Result result) { + return result == SlotActionExecutor.Result.MENU_NOT_READY + && mode == FlipperConfig.SlotAction.COPILOT_LEFT_CLICK + ? FlipperConfig.SlotAction.MENU_OPTION : null; + } + /** Transient failures consume this tick without permanently blocking the suggestion. */ boolean handleSlotActionFailure(SlotActionExecutor.Result result, SlotActionExecutor.Action action, String key) { if (result == SlotActionExecutor.Result.ACTED) return false; diff --git a/src/main/java/net/runelite/client/plugins/microbot/geflipper/SlotActionExecutor.java b/src/main/java/net/runelite/client/plugins/microbot/geflipper/SlotActionExecutor.java index 5b2df1d409..03cc925d10 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/geflipper/SlotActionExecutor.java +++ b/src/main/java/net/runelite/client/plugins/microbot/geflipper/SlotActionExecutor.java @@ -1,11 +1,12 @@ package net.runelite.client.plugins.microbot.geflipper; +import java.awt.Rectangle; import net.runelite.api.MenuAction; import net.runelite.api.MenuEntry; import net.runelite.api.Point; /** Executes supported slot operations without falling back to "View offer". */ -final class SlotActionExecutor { +public final class SlotActionExecutor { enum Action { ABORT("Abort offer", 2), MODIFY("Modify offer", 3); @@ -59,6 +60,17 @@ static boolean supportsAction(String[] actions, Action action) { && action.option.equals(actions[action.identifier - 1]); } + /** + * Planned click point and live cursor must both lie on the slot button. + * Exact pixel equality is not required: natural mouse often settles nearby after a hover. + */ + public static boolean cursorOnSlotButton(Point mouse, Point planned, Rectangle buttonBounds) { + return mouse != null && planned != null && buttonBounds != null + && buttonBounds.width >= 2 && buttonBounds.height >= 2 + && buttonBounds.contains(planned.getX(), planned.getY()) + && buttonBounds.contains(mouse.getX(), mouse.getY()); + } + /** The last menu entry is the actual left-click operation after Copilot's swap. */ static boolean matchesDefaultAction(MenuEntry[] entries, int slotId, Action action) { if (entries == null || entries.length == 0) return false; diff --git a/src/test/java/net/runelite/client/plugins/microbot/geflipper/SlotActionExecutorTest.java b/src/test/java/net/runelite/client/plugins/microbot/geflipper/SlotActionExecutorTest.java index f2b160b7bc..8a88f37d4c 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/geflipper/SlotActionExecutorTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/geflipper/SlotActionExecutorTest.java @@ -1,5 +1,6 @@ package net.runelite.client.plugins.microbot.geflipper; +import java.awt.Rectangle; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays;