Tool-Assisted-RS/src/main/kotlin/Agent.kt
2023-08-06 18:20:30 -04:00

431 lines
17 KiB
Kotlin

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