import java.awt.MouseInfo import java.awt.Point import java.awt.Robot import java.awt.event.InputEvent import java.util.concurrent.TimeUnit import kotlin.random.Random /** * Class providing methods for programmatic control of mouse and keyboard. * * This uses a Robot instance to facilitate actions like mouse movements, * clicks, scrolls, and key presses. * * It contains utility methods to perform actions in a human-like way, * including random pacing and wiggle to avoid robotic movement. * * Typical usage: * * ``` * val doer = Doer() * doer.mouseMove(Point(100, 200)) * doer.click(InputEvent.BUTTON1_DOWN_MASK) * ``` */ class Doer { /** * The Robot instance used to perform mouse and keyboard actions. */ private val robot = Robot() companion object { /** * Mouse button mask for a left mouse click. * * This stores the button mask value from [InputEvent] that represents a left mouse click. * * It can be passed to [click] or other methods that expect a mouse button value. */ private const val LEFT_CLICK = InputEvent.BUTTON1_DOWN_MASK /** * The duration in milliseconds of one "tick". * * This defines the concept of a "tick" as a unit of time used for pacing actions. * * It is used in methods like [sleepForNTicks] to calculate sleep durations * based on multiplying a number of ticks by this value. * * For example, 5 ticks with this value would be 5 * 600 = 3000ms sleep duration. */ const val TICK_DURATION_MS = 600L /** * Extra padding in milliseconds added before actions to account for latency. * * This defines an extra duration in milliseconds that is added to sleeps * and waits. * * It is to account for latency in the system before actions like mouse moves * and clicks actually take effect. */ const val LATENCY_PADDING_MS = 500L } /** * Data class to hold parameters for random wiggle amounts when moving the mouse. * * This holds the maximum x and y wiggle offset values to use when randomly * offsetting mouse movements to make them less robotic. * * For example, this could be passed to [getAlmostPoint] to control the randomization. * * @param xWiggle The max x offset, default 25. * @param yWiggle The max y offset, default 25. */ data class WiggleParams(val xWiggle: Int = 25, val yWiggle: Int = 25) /** * Moves the mouse cursor to the given [Point]. * * This uses the Robot [mouseMove] method to move the mouse cursor * to the x and y coordinates specified by the provided [Point] p. * * @param p The [Point] representing the x and y coordinates to move the mouse to. */ fun mouseMove(p: Point) { robot.mouseMove(p.x, p.y) } /** * Performs a mouse click with the specified mouse button. * * This uses the Robot [mousePress] and [mouseRelease] methods to click the * mouse button specified by [button]. * * It sleeps for a small random duration between press and release to add * variance to the click timing. * * @param button The mouse button to click, as a button mask from [InputEvent]. */ fun click(button: Int) { robot.mousePress(button) sleep(8, 8) robot.mouseRelease(button) } /** * Presses and releases a keyboard key. * * This uses the Robot [keyPress] and [keyRelease] methods to press * and release the key specified by [key]. * * It sleeps for a small random duration in between key press and release * to pace the keystrokes. * * @param key The key code of the key to press, from [java.awt.event.KeyEvent]. */ fun keypress(key: Int) { robot.keyPress(key) sleep(8, 8) robot.keyRelease(key) } /** * Moves the mouse to a point, left clicks, and sleeps. * * This moves the mouse to the given [Point] p, sleeps for 100ms ± 50ms, * left clicks using [LEFT_CLICK], and then sleeps for the given duration * dur ± durRange. * * @param p The [Point] to move the mouse to. * @param dur The base duration to sleep after clicking. * @param durRange The random variance to add to the sleep duration. */ fun moveMouseLeftClickAndSleep(p: Point, dur: Long, durRange: Long) { mouseMove(p) sleep(100, 50) click(LEFT_CLICK) sleep(dur, durRange) } /** * Sleeps for a duration proportional to the number of "ticks". * * This calculates the sleep duration based on multiplying the number of ticks [n] * by the tick duration constant [TICK_DURATION_MS]. * * It also adds a [LATENCY_PADDING_MS] to account for latency before actions take effect. * * Finally, some random variance is added by passing a range to [sleep]. * * @param n The number of "ticks" to calculate the sleep duration from. */ fun sleepForNTicks(n: Long) { val latencyPadding = LATENCY_PADDING_MS val baseWaitTime = n * TICK_DURATION_MS sleep(latencyPadding + baseWaitTime, 150) } /** * Counts down from a given number of seconds, calling a function on each step. * * This will count down the given number of [nSeconds], decrementing the count on each step. * * On each step, it will call the provided [announceFn] function, passing the current count. * * It waits 1 second between each call using [sleep]. * * @param nSeconds The number of seconds to count down from. * @param announceFn The function to call on each step, passing the current count. */ fun countDown(nSeconds: Int, announceFn: (step: Int) -> Unit) { for (i in nSeconds downTo 0) { announceFn(i) sleep(1000) } } /** * Gets a point near the given point with random offset. * * This takes in a base [Point] and returns a new point that is offset * randomly in the x and y directions. * * The maximum x and y offset amounts are determined by the [xWiggle] and [yWiggle] * values passed in [params]. * * The offset is calculated by: * - Generating random ints from 0 to xWiggle and 0 to yWiggle * - Randomly choosing 1 or -1 for the x and y offset directions * - Adding the offset to the original x and y coordinates * * @param p The base [Point] to offset from * @param params The [WiggleParams] controlling the max x and y offsets * @return A new [Point] offset randomly from the original point */ fun getAlmostPoint(p: Point, params: WiggleParams): Point { val xDel = Random.nextInt(0, params.xWiggle) val yDel = Random.nextInt(0, params.yWiggle) val xDir = if (Random.nextDouble() > 0.5) { 1 } else { -1 } val yDir = if (Random.nextDouble() > 0.5) { 1 } else { -1 } return Point(p.x + (xDel * xDir), p.y + (yDel + yDir)) } /** * Sleeps for the specified duration. * * This uses [TimeUnit.MILLISECONDS] to sleep for the given duration in milliseconds. * * @param dur The sleep duration in milliseconds. */ fun sleep(dur: Long) { TimeUnit.MILLISECONDS.sleep(dur) } /** * Sleeps for a duration with random variance added. * * This sleeps for the specified base duration plus a random amount of variance * to avoid having the exact same sleep duration each time. * * The variance is calculated by taking half of the variance value as the max * random duration to add. So a variance of 100 would add between 0 and 100ms following a normal distribution. * * @param dur The base duration to sleep in milliseconds. * @param variance The amount of random variance to add in milliseconds. */ fun sleep(dur: Long, variance: Long) { val dSize = (variance) / 2 val r1 = Random.nextLong(dSize) val r2 = Random.nextLong(dSize) sleep(dur + r1 + r2) } /** * Scrolls the mouse wheel down by one unit. * * Uses the Robot [mouseWheel] method to scroll up and then sleeps * for a random duration between 16-32ms to pace the scrolling. */ fun scrollOut(sleepDur: Long, sleepDurVariance: Long) { robot.mouseWheel(1) sleep(sleepDur, sleepDurVariance) } /** * Scrolls the mouse wheel up by one unit. * * Uses the Robot [mouseWheel] method to scroll up and then sleeps * for a random duration between 16-32ms to pace the scrolling. */ fun scrollIn(sleepDur: Long, sleepDurVariance: Long) { robot.mouseWheel(-1) sleep(sleepDur, sleepDurVariance) } /** * Gets the current mouse pointer location. * * This uses [MouseInfo.getPointerInfo] to retrieve the mouse pointer location * and returns it as a [Point]. * * @return The [Point] representing the mouse pointer's x and y coordinates. */ fun getPointerLocation(): Point { return MouseInfo.getPointerInfo().location } /** * Gets the mouse pointer location after a delay. * * This counts down the provided delay in seconds, printing a countdown prompt. * After the delay, it retrieves the current mouse pointer location using * [getPointerLocation]. * * @param delayInSeconds The amount of time in seconds to delay before getting the pointer location. * @return The [Point] representing the mouse x and y coordinates after the delay. */ fun getPointerLocationAfter(delayInSeconds: Int): Point { countDown(delayInSeconds) { print("\rtaking pointer snapshot in $it...") if (it == 0) { println("\r ") } } return getPointerLocation() } /** * Gets the current mouse pointer location and returns it as a Kotlin variable declaration string. * * This will wait for 5 seconds to allow the user to move the mouse to the desired location. * * The pointer location is retrieved using [getPointerLocationAfter] and converted to a * Kotlin 'val' variable declaration statement assigning it to a [Point]. * * For example, for varName "clickLoc": * * val clickLoc = Point(500, 400) * * @param varName The desired variable name to use in the declaration string. * @return The declaration string declaring a val with the pointer location. */ fun getPointerLocationAsValDeclarationString(varName: String): String { val info = getPointerLocationAfter(5) return "val $varName = Point(${info.x}, ${info.y})" } /** * Draws a star shape by moving the mouse to points around a center point. * * This calculates offset points around the provided center point * to trace out a star shape with the mouse cursor. * * The offset distance from the center can be configured by changing the offset constant. * * It will draw the star shape 10 times by looping through the point list. * * @param p The center point to base the star shape around. */ fun drawStar(p: Point) { val offset = 100 val top = Point(p.x, p.y - offset * 2) val topright = Point(p.x + offset * 2, p.y + offset) val bottomright = Point(p.x + offset * 2, p.y) val topleft = Point(p.x - offset * 2, p.y + offset) val bottomleft = Point(p.x - offset * 2, p.y) val points = arrayListOf(top, bottomleft, topright, topleft, bottomright) for (i in 0 until 10) { for (point in points) { mouseMove(point) sleep(32) } } } }