Tool-Assisted-RS/src/main/kotlin/Doer.kt
2023-08-03 11:37:14 -04:00

346 lines
12 KiB
Kotlin

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)
}
}
}
}