From 4ea3c009165fe96e331648b1bcb4bb48b789a24f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beatrice=20Dellac=C3=A0?= Date: Tue, 29 Sep 2026 12:02:42 +0200 Subject: [PATCH] run movements and clicks on virtual threads --- .../autosqueal/controls/CursorMover.java | 17 ++-- .../controls/SingleStepMovementTask.java | 93 ++++++++----------- .../beatrice/autosqueal/ui/MainWindow.java | 32 +++++-- 3 files changed, 71 insertions(+), 71 deletions(-) diff --git a/src/main/java/wtf/beatrice/autosqueal/controls/CursorMover.java b/src/main/java/wtf/beatrice/autosqueal/controls/CursorMover.java index bcdbd5e..51efcca 100644 --- a/src/main/java/wtf/beatrice/autosqueal/controls/CursorMover.java +++ b/src/main/java/wtf/beatrice/autosqueal/controls/CursorMover.java @@ -7,7 +7,8 @@ import wtf.beatrice.autosqueal.util.RunnerUtil; import java.awt.*; import java.security.SecureRandom; import java.util.Random; -import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; import java.util.function.BooleanSupplier; /** @@ -18,7 +19,7 @@ import java.util.function.BooleanSupplier; * using the machine, they are skipped, so the app never fights for the mouse. * * The movement itself is executed by a {@link SingleStepMovementTask}, which - * re-schedules itself on the shared scheduler until it reaches its destination. + * runs on its own virtual thread until it reaches its destination. */ public class CursorMover implements Runnable { @@ -27,14 +28,14 @@ public class CursorMover implements Runnable private static final int LOOPS_BEFORE_CLICK = 5; private final Random random = new SecureRandom(); - private final ScheduledExecutorService scheduler; + private final ExecutorService movementExecutor; private final RobotMouseTracker robotTracker; private final BooleanSupplier isUserAway; private int iteration = 0; - public CursorMover(ScheduledExecutorService scheduler, RobotMouseTracker robotTracker, BooleanSupplier isUserAway) { - this.scheduler = scheduler; + public CursorMover(ExecutorService movementExecutor, RobotMouseTracker robotTracker, BooleanSupplier isUserAway) { + this.movementExecutor = movementExecutor; this.robotTracker = robotTracker; this.isUserAway = isUserAway; } @@ -71,10 +72,12 @@ public class CursorMover implements Runnable LOGGER.info("Destination coordinates: {}, {}", destX, destY); try { - SingleStepMovementTask movement = new SingleStepMovementTask(scheduler, robotTracker, destX, destY, click); - scheduler.execute(movement); + SingleStepMovementTask movement = new SingleStepMovementTask(robotTracker, destX, destY, click); + movementExecutor.execute(movement); } catch (AWTException ex) { LOGGER.error("Could not start movement task", ex); + } catch (RejectedExecutionException ex) { + LOGGER.debug("Movement not started: automation is shutting down"); } } } \ No newline at end of file diff --git a/src/main/java/wtf/beatrice/autosqueal/controls/SingleStepMovementTask.java b/src/main/java/wtf/beatrice/autosqueal/controls/SingleStepMovementTask.java index fe16dca..915ce8f 100644 --- a/src/main/java/wtf/beatrice/autosqueal/controls/SingleStepMovementTask.java +++ b/src/main/java/wtf/beatrice/autosqueal/controls/SingleStepMovementTask.java @@ -5,16 +5,13 @@ import org.apache.logging.log4j.Logger; import java.awt.*; import java.awt.event.InputEvent; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; /** - * Moves the mouse cursor towards a destination one small step at a time, - * re-scheduling itself on the shared scheduler until the destination is reached. + * Moves the mouse cursor towards a destination one small step at a time. * - * When it gets there, it can optionally perform a double click, scheduled as - * separate one-shot actions so that the scheduler is never blocked. + * Runs on its own virtual thread: it mostly sleeps between steps, so blocking + * it is free. When it gets to its destination, it can optionally perform a + * double click, with the pacing the app has always used. * * All the movements it performs are reported to the {@link RobotMouseTracker}, * so that the away detection can tell them apart from the user's. @@ -26,7 +23,6 @@ public class SingleStepMovementTask implements Runnable { /** Delay between two consecutive cursor steps, in milliseconds. */ private static final long STEP_DELAY_MILLISECONDS = 2L; - private final ScheduledExecutorService scheduler; private final RobotMouseTracker robotTracker; private final Robot robot; private final int destX; @@ -38,9 +34,8 @@ public class SingleStepMovementTask implements Runnable { private float stepX; private float stepY; - public SingleStepMovementTask(ScheduledExecutorService scheduler, RobotMouseTracker robotTracker, int destinationX, int destinationY, boolean click) throws AWTException { + public SingleStepMovementTask(RobotMouseTracker robotTracker, int destinationX, int destinationY, boolean click) throws AWTException { - this.scheduler = scheduler; this.robotTracker = robotTracker; this.destX = destinationX; this.destY = destinationY; @@ -69,37 +64,29 @@ public class SingleStepMovementTask implements Runnable { robotTracker.moveStarted(); try { - tick(); - } catch (RuntimeException ex) { - // whatever happened, don't leave the tracker stuck on "moving" - LOGGER.error("Unexpected error while moving the cursor", ex); - robotTracker.moveEnded(Math.round(currentX), Math.round(currentY)); - } - } + while (!hasReachedDestination()) { + // when less than a full step is left, move exactly what is + // left, so that both axes always land on their destination + stepX = adjustedStep(currentX, destX, stepX); + stepY = adjustedStep(currentY, destY, stepY); - private void tick() { + currentX = advance(currentX, destX, stepX); + currentY = advance(currentY, destY, stepY); + + robot.mouseMove(Math.round(currentX), Math.round(currentY)); + + Thread.sleep(STEP_DELAY_MILLISECONDS); + } - if (hasReachedDestination()) { onDestinationReached(); - return; - } - - // when less than a full step is left, move exactly what is left, - // so that both axes always land exactly on their destination - stepX = adjustedStep(currentX, destX, stepX); - stepY = adjustedStep(currentY, destY, stepY); - - currentX = advance(currentX, destX, stepX); - currentY = advance(currentY, destY, stepY); - - robot.mouseMove(Math.round(currentX), Math.round(currentY)); - - try { - scheduler.schedule(this, STEP_DELAY_MILLISECONDS, TimeUnit.MILLISECONDS); - } catch (RejectedExecutionException ex) { - // the scheduler was shut down mid-movement: the automation was - // stopped, so just end the movement chain here - LOGGER.debug("Movement interrupted: scheduler is shut down"); + } catch (InterruptedException ex) { + // the movement was interrupted: the automation was stopped, so + // just end the movement here + Thread.currentThread().interrupt(); + LOGGER.debug("Movement interrupted"); + robotTracker.moveEnded(Math.round(currentX), Math.round(currentY)); + } catch (RuntimeException ex) { + LOGGER.error("Unexpected error while moving the cursor", ex); robotTracker.moveEnded(Math.round(currentX), Math.round(currentY)); } } @@ -108,35 +95,29 @@ public class SingleStepMovementTask implements Runnable { return Math.round(currentX) == destX && Math.round(currentY) == destY; } - private void onDestinationReached() { + private void onDestinationReached() throws InterruptedException { LOGGER.info("Reached destination [{}, {}], stopping mover", destX, destY); robotTracker.moveEnded(destX, destY); if (click) { - scheduleClickSequence(); + performClickSequence(); } } /** - * Schedules a double click with the same pacing the app has always used: + * Performs a double click with the same pacing the app has always used: * press at +500ms, release at +700ms, press at +1200ms, release at +1400ms. */ - private void scheduleClickSequence() { - scheduleClick(500L, true); - scheduleClick(700L, false); - scheduleClick(1200L, true); - scheduleClick(1400L, false); - } - - private void scheduleClick(long delayMillis, boolean press) { - scheduler.schedule(() -> { - if (press) { - robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); - } else { - robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); - } - }, delayMillis, TimeUnit.MILLISECONDS); + private void performClickSequence() throws InterruptedException { + Thread.sleep(500L); + robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); + Thread.sleep(200L); + robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); + Thread.sleep(500L); + robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); + Thread.sleep(200L); + robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); } /** diff --git a/src/main/java/wtf/beatrice/autosqueal/ui/MainWindow.java b/src/main/java/wtf/beatrice/autosqueal/ui/MainWindow.java index 04976d6..7c4ec18 100644 --- a/src/main/java/wtf/beatrice/autosqueal/ui/MainWindow.java +++ b/src/main/java/wtf/beatrice/autosqueal/ui/MainWindow.java @@ -15,6 +15,7 @@ import java.awt.*; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -32,7 +33,8 @@ public class MainWindow private final JFrame frame = new JFrame(); private Button toggleButton; - private ScheduledExecutorService scheduler; + private ScheduledExecutorService periodicScheduler; + private ExecutorService movementExecutor; private CursorMover cursorMover; /** @@ -142,21 +144,28 @@ public class MainWindow private void startAutomation() { LOGGER.info("Starting automation"); - scheduler = Executors.newScheduledThreadPool(3, runnable -> { + // periodic work (polling and movement cadence) on a classic scheduler, + // so that it can be scheduled with fixed delays + periodicScheduler = Executors.newScheduledThreadPool(2, runnable -> { Thread thread = new Thread(runnable, "autosqueal-scheduler"); thread.setDaemon(true); return thread; }); + // movements and clicks run on virtual threads: they mostly sleep, + // and blocking them costs nothing + movementExecutor = Executors.newThreadPerTaskExecutor( + Thread.ofVirtual().name("autosqueal-movement-").factory()); + // away-detection: polls the cursor position and ignores the movements // the app performs itself, as reported by the robot mouse tracker. // the automation only moves the mouse when the user is away. RobotMouseTracker robotTracker = new RobotMouseTracker(); CursorMoveListener cursorMoveListener = new CursorMoveListener(robotTracker); - scheduler.scheduleWithFixedDelay(cursorMoveListener, 0L, AWAY_POLL_INTERVAL_SECONDS, TimeUnit.SECONDS); + periodicScheduler.scheduleWithFixedDelay(cursorMoveListener, 0L, AWAY_POLL_INTERVAL_SECONDS, TimeUnit.SECONDS); - cursorMover = new CursorMover(scheduler, robotTracker, cursorMoveListener::isUserAway); - scheduler.scheduleWithFixedDelay(cursorMover, + cursorMover = new CursorMover(movementExecutor, robotTracker, cursorMoveListener::isUserAway); + periodicScheduler.scheduleWithFixedDelay(cursorMover, AUTOMATION_START_DELAY_SECONDS, RunnerUtil.SECONDS_BETWEEN_MOVES, TimeUnit.SECONDS); @@ -165,10 +174,17 @@ public class MainWindow private void stopAutomation() { LOGGER.info("Stopping automation"); - if (scheduler != null) { - scheduler.shutdownNow(); - scheduler = null; + if (periodicScheduler != null) { + periodicScheduler.shutdownNow(); + periodicScheduler = null; } + + if (movementExecutor != null) { + // interrupts the in-flight movement threads, too + movementExecutor.shutdownNow(); + movementExecutor = null; + } + cursorMover = null; }