import java.awt.Point import java.awt.event.InputEvent import java.awt.event.KeyEvent /** * Agent handles lower level actions like banking, navigation, and timing * to support automation routines. * * This provides a common set of primitive actions that can be used across * different automation workflows. Routines build on top of these actions. * * The agent handles things like: * - Bank interaction (presets, deposits, withdrawals) * - Navigation points and travel * - Timing actions with randomness * - Progress reporting and logging * * By encapsulating these common actions, routines can focus on their * specific workflow logic and leverage the agent for the lower level * activities needed to craft, cook, etc. */ class Agent(val controller: Automaton = RobotController()) { companion object { /** * Extra padding in milliseconds added before actions to account for latency. 500ms is entirely arbitrary. It is * simply a value that works well during high-load periods. Better to be conservative than lossy. * * 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 /** * The duration in milliseconds of one "tick". The duration of 600ms matches the tick duration of game servers. * * 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 /** * Performs a standing crafting task loop for the agent. * * The agent will repeatedly go to the bank, withdraw items, * open the crafting interface, craft items, close it, * and deposit items back until the total volume is reached. * * @param params the parameters configuring how to perform the standing task */ fun doStandingTask(params: StandingTaskParams) { val agent = params.agent agent.doLoop(params.totalVolume, params.volumePerStep) { agent.bankStandForLoop( params.bankPoint, params.bankPresetHotkey, params.craftingDialogHotkey, params.craftingWaitDurationMillis, params.craftingWaitDurationVarianceMillis ) } } /** * Performs a looped travel task for the agent. * * This handles having the agent repeatedly travel from the bank * to a station, process items, and travel back. * * @param params the parameters configuring how to perform the task */ fun doTravelTask(params: TravelTaskParams) { val agent = params.agent agent.doLoop(params.totalVolume, params.volumePerStep) { agent.processAtStationNearBank( params.bankPoint, params.travelPoint, params.bankPresetHotkey, params.travelDurationMillis, params.travelDurationVarianceMillis, params.craftingWaitDurationMillis, params.craftingWaitDurationVarianceMillis ) } } } /** * Computes the total number of steps needed to process the given total volume. * * @param totalVolume the total amount that needs to be processed * @param volumePerStep the amount to process per step * @return the number of steps required to process the total volume */ private fun computeTotalSteps(totalVolume: Int, volumePerStep: Int) = totalVolume / volumePerStep + if (totalVolume % volumePerStep > 0) { 1 } else { 0 } /** * Performs a loop to repeatedly execute a task for automated steps. * * @param totalVolume The total number of units to process across all steps. * @param volumePerStep The number of units to process per step. * @param task The task function to execute on each step. It receives the Agent. * * This method handles iterating through the steps, reporting progress, * timing the overall execution, and calling the task on each iteration. * * It uses computeTotalSteps to calculate how many iterations needed based on total * and per step volumes. The task typically simulates some action like banking. */ fun doLoop(totalVolume: Int, volumePerStep: Int, task: (Agent) -> Unit) { require(totalVolume > 0) { "You must make at least 1 thing in total" } require(volumePerStep > 0) { "You must consume at least 1 thing per step" } val totalSteps = computeTotalSteps(totalVolume, volumePerStep) val start = System.currentTimeMillis() for (i in 0 until totalSteps) { report(i + 1, totalSteps, System.currentTimeMillis() - start) task(this) } println() val finish = System.currentTimeMillis() println("Finished everything in ${prettyTimeString(finish - start)}") } /** * Prints a progress report for the current step. * * @param step The current step number. * @param of The total number of steps. * @param dur The duration in milliseconds so far. * * This method prints a progress report to the console in the format: * "Step {step} of {of} ({formattedDuration} complete | ~{remainingTime} remaining)" * * It calculates the estimated remaining time based on the current duration and * number of steps completed vs total steps. * * The output is printed on the same line to dynamically update the progress. */ fun report(step: Int, of: Int, dur: Long) { val remaining = (dur / step) * (of - step) print("\rStep $step of $of (${prettyTimeString(dur)} complete\t|\t~${prettyTimeString(remaining)} remaining) ") } /** * Formats the given duration in milliseconds into a human readable time string. * * @param durationMillis The duration to format in milliseconds. * * @return A formatted time string showing the duration broken down into hours, minutes, and seconds. * * This converts the duration into hours, minutes, and seconds based on millis per second, * minute, and hour constants. It returns a string in the format "XhYmZs" where X, Y, and Z are * the calculated hours, minutes, and seconds. * * If durationMillis is 0, it returns "No time data yet" instead. */ fun prettyTimeString(durationMillis: Long): String { if (durationMillis == 0L) { return "No time data yet" } val millisPerSecond = 1000L val millisPerMinute = 60L * millisPerSecond val millisPerHour = 60L * millisPerMinute return if (durationMillis > millisPerHour) { return "${durationMillis / millisPerHour}h${(durationMillis % millisPerHour) / millisPerMinute}m${(durationMillis % millisPerMinute) / millisPerSecond}s" } else if (durationMillis > millisPerMinute) { return "${(durationMillis % millisPerHour) / millisPerMinute}m${(durationMillis % millisPerMinute) / millisPerSecond}s" } else { "${(durationMillis % millisPerMinute) / millisPerSecond}s" } } /** * Prompts the user to position the mouse pointer and returns the pointer location. * * @param prompt The message to display to the user as a prompt. * * @return The Point containing the mouse pointer location after prompting. * * This first prints the prompt message to the console. * It then uses the Doer to count down from 5 seconds while printing a countdown. * After the countdown, it gets the current mouse pointer location using the Doer * and returns the Point. * * This allows prompting the user to move the mouse before taking a snapshot of the pointer position. */ fun promptUserForPoint(prompt: String): Point { println(prompt) countDown(5) { print("\rtaking point snapshot in $it... ") if (it == 0) { println("\r ") } } return controller.getPointerLocation() } /** * Scrolls out to the specified height by scrolling in and then back out. * * @param height The height in pixels to scroll out to. * @param scrollWaitAndVariance The number of milliseconds to wait between scroll steps. Defaults to 16. * * This method first sleeps for 1 second. It then scrolls in by the height amount * using repeated small scroll in steps. * * After scrolling in fully, it then scrolls back out using a few repeated small scroll outs. * * This allows smoothly animating the scroll all the way in and then back out to a specific height * * The scrollWaitAndVariance controls the pacing between scroll steps. The default of 16ms provides * a reasonable scroll animation speed. */ fun scrollOutToHeight(height: Int, scrollWaitAndVariance: Long = 10L) { controller.sleep(1000) for (i in 0 until height * 2) { controller.scrollIn(scrollWaitAndVariance, scrollWaitAndVariance) } for (i in 0 until height) { controller.scrollOut(scrollWaitAndVariance, scrollWaitAndVariance) } } /** * Performs automated banking steps in a loop at the given chest location. * * @param chest The Point location of the bank chest to click on. * @param bankPresetHotkey The key for the inventory preset to withdraw. * @param craftingDialogueHotkey The key for the crafting preset to open. * @param waitDurationMillis The time in ms to wait at each loop iteration. * @param waitDurationVariance Random variance to add to wait time. * * This handles the steps to open the bank interface, withdraw a preset, * open a crafting interface, and wait for the desired duration. * * It clicks the bank chest, presses the inventory hotkey, presses the crafting hotkey, * presses spacebar to accept, and waits before repeating. * * Useful for automated banking behaviors like crafting or herblore. */ private fun bankStandForLoop( chest: Point, bankPresetHotkey: Int, craftingDialogueHotkey: Int, waitDurationMillis: Long, waitDurationVariance: Long ) { //open the bank located by the chest parameter moveMouseLeftClickAndSleep(controller.getAlmostPoint(chest, WiggleParams()), 900, 400) //withdraw the desired inventory preset controller.keyPress(bankPresetHotkey) //sleep for a server tick sleepForNTicks(1) //open the crafting dialog with the correct hotkey controller.keyPress(craftingDialogueHotkey) //sleep for a server tick sleepForNTicks(1) //press the "accept" default hotkey controller.keyPress(KeyEvent.VK_SPACE) //wait for the desired time to finish controller.sleepWithVariance(waitDurationMillis, waitDurationVariance) } /** * Performs banking actions at a bank without additional dialog prompts. * * @param chest The Point location of the bank chest or stand to interact with. * @param invKey The key code for the inventory preset hotkey to withdraw. * @param hotKey The key code for the action hotkey, like crafting. * * This method handles clicking the chest, withdrawing a preset inventory, * and activating a process like crafting that doesn't require additional prompts. * * It clicks the chest location, presses the inventory preset hotkey, waits briefly, * then presses the action hotkey like crafting. This allows automated crafting at the bank. * * The sleeps provide a brief pause between actions to allow animations. */ fun bankStandWithoutDialog(chest: Point, invKey: Int, hotKey: Int) { //open the bank located by the chest parameter moveMouseLeftClickAndSleep(controller.getAlmostPoint(chest, WiggleParams()), 900, 400) //withdraw the desired inventory preset controller.keyPress(invKey) sleepForNTicks(1) //press the hotkey that causes action without dialogue controller.keyPress(hotKey) sleepForNTicks(1) } /** * Performs processing steps between a bank and nearby crafting station. * * @param chest The Point location of the bank chest. * @param station The Point location of the processing station. * @param bankPresetHotkey The inventory preset hotkey to withdraw at bank. * @param travelDurationMillis Base travel time between bank and station. * @param travelDurationVarianceMillis Random variance to add to travel time. * @param waitDurationMillis Base wait time for processing at station. * @param waitDurationVarianceMillis Random variance to add to wait time. * * This handles the steps to go to the bank, withdraw a preset, go to the station, * open the processing interface, wait, and then loop. * * It goes between the bank and station locations, simulating travel time. * At the bank it withdraws using the preset hotkey. * At the station it activates processing and waits. */ fun processAtStationNearBank( chest: Point, station: Point, bankPresetHotkey: Int, travelDurationMillis: Long, travelDurationVarianceMillis: Long, waitDurationMillis: Long, waitDurationVarianceMillis: Long ) { //move to the bank and open the interface moveMouseLeftClickAndSleep( controller.getAlmostPoint(chest, WiggleParams()), travelDurationMillis, travelDurationVarianceMillis ) //withdraw desired loadout controller.keyPress(bankPresetHotkey) sleepForNTicks(1) //move to station and open the crafting dialog moveMouseLeftClickAndSleep(station, travelDurationMillis, travelDurationVarianceMillis) //start the crafting task controller.keyPress(KeyEvent.VK_SPACE) //wait for it to complete controller.sleepWithVariance(waitDurationMillis, waitDurationVarianceMillis) } /*============================================================================================================== cheater functions ==============================================================================================================*/ fun getBankPoint(): Point { return promptUserForPoint("Hold your mouse over the bank...") } fun countDown(nSeconds: Int, announceFn: (step: Int) -> Unit) { for (i in nSeconds downTo 0) { announceFn(i) controller.sleep(1000) } } fun getPointerLocationAfter(delayInSeconds: Int): Point { countDown(delayInSeconds) { print("\rtaking pointer snapshot in $it...") if (it == 0) { println("\r ") } } return controller.getPointerLocation() } fun getPointerLocationAsValDeclarationString(varName: String): String { val info = getPointerLocationAfter(5) return "val $varName = Point(${info.x}, ${info.y})" } fun moveMouseLeftClickAndSleep(p: Point, dur: Long, durRange: Long) { controller.moveMouse(p) controller.sleepWithVariance(100, 50) //left click controller.mouseClick(InputEvent.BUTTON1_DOWN_MASK) controller.sleepWithVariance(dur, durRange) } fun sleepForNTicks(n: Long) { val latencyPadding = LATENCY_PADDING_MS val baseWaitTime = n * TICK_DURATION_MS controller.sleepWithVariance(latencyPadding + baseWaitTime, 150) } 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) { controller.moveMouse(point) controller.sleep(32) } } } }