VisionController can now pipe out visual stuff
This commit is contained in:
parent
437a3b9fee
commit
c7596a7b85
@ -10,5 +10,5 @@ interface OSProxy {
|
|||||||
|
|
||||||
fun setForegroundWindowByName(name: String)
|
fun setForegroundWindowByName(name: String)
|
||||||
|
|
||||||
fun getForegroundWindowBounds(): Rectangle?
|
fun getScaledForegroundWindowBounds(): Rectangle?
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,52 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import controllers.windows.WindowsOSProxy
|
import controllers.windows.WindowsOSProxy
|
||||||
import java.nio.file.Paths
|
import java.awt.image.BufferedImage
|
||||||
import javax.imageio.ImageIO
|
|
||||||
|
|
||||||
interface VisionController: Automaton, OSProxy {
|
/**
|
||||||
fun takeScreenshot()
|
* Interface for a vision controller that can take screenshots.
|
||||||
|
*
|
||||||
|
* A vision controller provides capabilities for automated visual
|
||||||
|
* interactions like taking screenshots.
|
||||||
|
*
|
||||||
|
* Implementations will provide OS-specific implementations for taking
|
||||||
|
* screenshots of the foreground window.
|
||||||
|
*/
|
||||||
|
interface VisionController : Automaton, OSProxy {
|
||||||
|
/**
|
||||||
|
* Takes a screenshot of the current foreground window.
|
||||||
|
*
|
||||||
|
* The implementation should use the appropriate OS APIs to get the
|
||||||
|
* window bounds and take a screenshot of the window.
|
||||||
|
*
|
||||||
|
* @return A BufferedImage containing the screenshot of the foreground
|
||||||
|
* window.
|
||||||
|
*/
|
||||||
|
fun takeScreenshotOfForeground(): BufferedImage
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConcreteVisionController(): VisionController, WindowsOSProxy, RobotAutomaton(){
|
/**
|
||||||
override fun takeScreenshot() {
|
* Concrete implementation of the [VisionController] interface.
|
||||||
val rect = getForegroundWindowBounds()
|
* Implements the interface methods by extending [WindowsOSProxy] to get
|
||||||
val img = robot.createScreenCapture(rect)
|
* OS-specific functionality on Windows and [RobotAutomaton] to get access
|
||||||
val testPath = Paths.get(".", "test2.png")
|
* to the Robot class for taking automated screenshots.
|
||||||
ImageIO.write(img, "png", testPath.toFile())
|
*/
|
||||||
|
class ConcreteVisionController : VisionController, WindowsOSProxy, RobotAutomaton() {
|
||||||
|
/**
|
||||||
|
* Takes a screenshot of the foreground window scaled for high DPI.
|
||||||
|
*
|
||||||
|
* Gets the bounding rectangle of the current foreground window using
|
||||||
|
* [getScaledForegroundWindowBounds].
|
||||||
|
*
|
||||||
|
* Then uses the [RobotAutomaton.robot] to create a screenshot cropped to that
|
||||||
|
* rectangle area.
|
||||||
|
*
|
||||||
|
* @return A [BufferedImage] containing a screenshot of the foreground window
|
||||||
|
* scaled to account for the screen DPI.
|
||||||
|
*/
|
||||||
|
override fun takeScreenshotOfForeground(): BufferedImage {
|
||||||
|
val rect = getScaledForegroundWindowBounds()
|
||||||
|
return robot.createScreenCapture(rect)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package controllers.windows
|
package controllers.windows
|
||||||
|
|
||||||
import com.sun.jna.Native
|
import com.sun.jna.Native
|
||||||
import com.sun.jna.NativeLong
|
|
||||||
import com.sun.jna.Pointer
|
import com.sun.jna.Pointer
|
||||||
import com.sun.jna.win32.StdCallLibrary
|
import com.sun.jna.win32.StdCallLibrary
|
||||||
import controllers.windows.WindowsOSProxy.WinRect
|
import controllers.windows.WindowsOSProxy.WinRect
|
||||||
@ -146,6 +145,34 @@ interface User32 : StdCallLibrary {
|
|||||||
fun GetWindowRect(hWnd: Pointer?, lpRect: Pointer?): Boolean
|
fun GetWindowRect(hWnd: Pointer?, lpRect: Pointer?): Boolean
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets information about the specified window.
|
||||||
|
*
|
||||||
|
* This calls the Win32 API GetWindowLongA function to get configuration
|
||||||
|
* information about the window handle [hWnd].
|
||||||
|
*
|
||||||
|
* The [nIndex] parameter specifies what window information to retrieve:
|
||||||
|
*
|
||||||
|
* - GWL_STYLE: Retrieves the window style.
|
||||||
|
* - GWL_EXSTYLE: Retrieves the extended window style.
|
||||||
|
* - GWL_ID: Retrieves the control ID.
|
||||||
|
*
|
||||||
|
* And more (see WinUser.h documentation).
|
||||||
|
*
|
||||||
|
* Usage example:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* val hwnd = findWindow() // Get some window handle
|
||||||
|
*
|
||||||
|
* val style = GetWindowLongA(hwnd, GWL_STYLE)
|
||||||
|
*
|
||||||
|
* println("Window style: $style")
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param hWnd The window handle (HWND) to get info about.
|
||||||
|
* @param nIndex The info index to retrieve.
|
||||||
|
* @return The requested window information.
|
||||||
|
*/
|
||||||
fun GetWindowLongA(hWnd: Pointer?, nIndex: Int): Int
|
fun GetWindowLongA(hWnd: Pointer?, nIndex: Int): Int
|
||||||
|
|
||||||
|
|
||||||
@ -162,7 +189,32 @@ interface User32 : StdCallLibrary {
|
|||||||
*/
|
*/
|
||||||
fun GetDpiForSystem(): Int
|
fun GetDpiForSystem(): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the DPI for the specified window.
|
||||||
|
*
|
||||||
|
* This calls the Win32 API GetDpiForWindow function to get the DPI (dots per inch)
|
||||||
|
* value for the given window handle [hWnd].
|
||||||
|
*
|
||||||
|
* The DPI indicates the display resolution and is used for scaling elements
|
||||||
|
* appropriately on high resolution screens.
|
||||||
|
*
|
||||||
|
* For example, a 96 DPI screen means 96 pixels per inch. A 144 DPI screen has
|
||||||
|
* higher pixel density, so elements need to be scaled up in size accordingly.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* val hwnd = findWindow() // Get some window handle
|
||||||
|
* val dpi = GetDpiForWindow(hwnd)
|
||||||
|
*
|
||||||
|
* // Scale based on DPI
|
||||||
|
* val scale = dpi / 96f
|
||||||
|
* drawScaledElements(scale)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param hWnd The window handle (HWND) to get the DPI for.
|
||||||
|
* @return The DPI value for the given window.
|
||||||
|
*/
|
||||||
fun GetDpiForWindow(hWnd: Pointer?): Int
|
fun GetDpiForWindow(hWnd: Pointer?): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -223,46 +223,71 @@ interface WindowsOSProxy : MousePointerObserver, OSProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the screen bounds of the current foreground window.
|
* Gets the screen bounds of the foreground window scaled for high DPI screens.
|
||||||
*
|
*
|
||||||
* Calls the Win32 API [User32.GetForegroundWindow] to get the HWND of the
|
* Calls Win32 APIs to get the window handle (HWND) of the foreground window
|
||||||
* foreground window.
|
* using [User32.GetForegroundWindow].
|
||||||
*
|
*
|
||||||
* Then calls [User32.GetWindowRect] to populate a [WinRect] struct with the
|
* Then calls [User32.GetWindowRect] to get the outer bounding rectangle
|
||||||
* window bounds.
|
* coordinates of the window and stores them in a [WinRect] struct.
|
||||||
*
|
*
|
||||||
* The rectangle coordinates are converted to a [Rectangle] and returned. This allows us to keep the implementation
|
* To support high DPI screens, the [User32.GetDpiForWindow] API is called to
|
||||||
* generic and constrained within the java std library.
|
* get the DPI scaling for the window. The rectangle coordinates are scaled by
|
||||||
* In the future we may want to add LinuxOSProxy or DarwinOSProxy.
|
* multiplying by the default DPI (96) and dividing by the actual DPI.
|
||||||
*
|
*
|
||||||
* @return The outer bounding rectangle of the foreground window in screen coordinates,
|
* The scaled rectangle coordinates are returned encapsulated in a [Rectangle]
|
||||||
* or an empty [Rectangle] if it failed.
|
* to provide a coordinate system agnostic result.
|
||||||
|
*
|
||||||
|
* @return The outer bounding rectangle of the foreground window scaled for the
|
||||||
|
* screen DPI, or null if it failed.
|
||||||
*/
|
*/
|
||||||
override fun getForegroundWindowBounds(): Rectangle? {
|
override fun getScaledForegroundWindowBounds(): Rectangle? {
|
||||||
val user32 = User32.INSTANCE
|
val user32 = User32.INSTANCE
|
||||||
val hWnd = user32.GetForegroundWindow()
|
val hWnd = user32.GetForegroundWindow()
|
||||||
|
|
||||||
val rect = getRectFromWindowHandle(user32, hWnd)
|
val rect = getRectFromWindowHandle(user32, hWnd)
|
||||||
|
val defaultDPI = 96
|
||||||
|
|
||||||
return if (rect != null) {
|
return if (rect != null) {
|
||||||
|
// Get the DPI for the given window
|
||||||
|
val dpi = user32.GetDpiForWindow(hWnd)
|
||||||
|
|
||||||
//we need to make the calls to get the info to correct for the dpi scaling
|
require(dpi != 0)
|
||||||
val secondarySuccess = correctWinRectForDpi(user32, hWnd!!, rect.pointer)
|
|
||||||
|
|
||||||
if (secondarySuccess) {
|
// Default DPI is 96. Scale the coordinates based on the actual DPI. This handles cases where screen has
|
||||||
//the correctWinRectForDpi function doesn't have access to the underlying stuct in order to read it, so
|
// high DPI/scaling.
|
||||||
// we have to call this
|
|
||||||
rect.read()
|
|
||||||
Rectangle(rect.top, rect.left, (rect.right - rect.left), (rect.bottom - rect.top))
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Calculate width by getting difference between right and left edges Then multiply by default DPI and
|
||||||
|
// divide by actual DPI to scale
|
||||||
|
val width = ((rect.right - rect.left) * defaultDPI) / dpi
|
||||||
|
|
||||||
|
// Same for height - get difference between bottom and top edges Multiply by default DPI and divide by
|
||||||
|
// actual to scale
|
||||||
|
val height = ((rect.bottom - rect.top) * defaultDPI) / dpi
|
||||||
|
Rectangle(rect.top, rect.left, width, height)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the window rectangle coordinates for the given window handle.
|
||||||
|
*
|
||||||
|
* This calls the Win32 API [User32.GetWindowRect] function to populate a
|
||||||
|
* [WinRect] struct with the window coordinates.
|
||||||
|
*
|
||||||
|
* It first creates an instance of [WinRect] to hold the results.
|
||||||
|
* [User32.GetWindowRect] is called, passing the window handle [hWnd] and
|
||||||
|
* pointer to the [WinRect].
|
||||||
|
*
|
||||||
|
* If it succeeds, the [WinRect] values are read back out since they are
|
||||||
|
* populated in native memory. The [WinRect] is returned.
|
||||||
|
*
|
||||||
|
* If it fails, null is returned.
|
||||||
|
*
|
||||||
|
* @param user32 An instance of User32, used to call the Win32 API.
|
||||||
|
* @param hWnd The window handle to get the rect for.
|
||||||
|
* @return The WindowRect with populated coordinates, or null if failed.
|
||||||
|
*/
|
||||||
private fun getRectFromWindowHandle(user32: User32, hWnd: Pointer?): WinRect? {
|
private fun getRectFromWindowHandle(user32: User32, hWnd: Pointer?): WinRect? {
|
||||||
//we have to provide the system a native struct in order to hold the results of our request
|
//we have to provide the system a native struct in order to hold the results of our request
|
||||||
val rect = WinRect()
|
val rect = WinRect()
|
||||||
@ -278,18 +303,4 @@ interface WindowsOSProxy : MousePointerObserver, OSProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun correctWinRectForDpi(user32: User32, hWnd: Pointer, lpRect: Pointer): Boolean {
|
|
||||||
val user32 = User32.INSTANCE
|
|
||||||
val dwStyle = user32.GetWindowLongA(hWnd, User32.GWL_STYLE)
|
|
||||||
|
|
||||||
|
|
||||||
val bMenu = true
|
|
||||||
|
|
||||||
val dwExStyle = user32.GetWindowLongA(hWnd, User32.GWL_EXSTYLE)
|
|
||||||
|
|
||||||
val dpi = user32.GetDpiForWindow(hWnd)
|
|
||||||
|
|
||||||
return user32.AdjustWindowRectExForDpi(lpRect, dwStyle, bMenu, dwExStyle, dpi)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -198,7 +198,7 @@ class RSAgent(override val automaton: Automaton = RobotAutomaton()) : RSOrchestr
|
|||||||
*
|
*
|
||||||
* @param n The number of game ticks to sleep for.
|
* @param n The number of game ticks to sleep for.
|
||||||
*/
|
*/
|
||||||
fun sleepForNTicks(n: Long) {
|
override fun sleepForNTicks(n: Long) {
|
||||||
val latencyPadding = LATENCY_PADDING_MS
|
val latencyPadding = LATENCY_PADDING_MS
|
||||||
val baseWaitTime = n * TICK_DURATION_MS
|
val baseWaitTime = n * TICK_DURATION_MS
|
||||||
automaton.sleepWithVariance(latencyPadding + baseWaitTime, 150)
|
automaton.sleepWithVariance(latencyPadding + baseWaitTime, 150)
|
||||||
|
|||||||
@ -161,4 +161,16 @@ interface RSOrchestrator : Orchestrator {
|
|||||||
* @return The Point representing the x,y screen coordinates of the bank location.
|
* @return The Point representing the x,y screen coordinates of the bank location.
|
||||||
*/
|
*/
|
||||||
fun getBankPoint(): Point
|
fun getBankPoint(): Point
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pauses execution for the specified number of game ticks.
|
||||||
|
*
|
||||||
|
* This will sleep/wait for the given number of game ticks before continuing.
|
||||||
|
*
|
||||||
|
* Game ticks typically occur around once every 0.6 seconds. So this can be used
|
||||||
|
* to pause for a duration measured in game ticks rather than absolute time.
|
||||||
|
*
|
||||||
|
* @param n The number of game ticks to wait/sleep for.
|
||||||
|
*/
|
||||||
|
fun sleepForNTicks(n: Long)
|
||||||
}
|
}
|
||||||
@ -417,6 +417,17 @@ object RunescapeRoutines {
|
|||||||
RSOrchestrator.doTravelTask(agent, params)
|
RSOrchestrator.doTravelTask(agent, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crafts a specified volume of necromancy ink at the bank.
|
||||||
|
*
|
||||||
|
* This handles the entire workflow of crafting necromancy ink from scratch:
|
||||||
|
* - Withdrawing bank preset on F6
|
||||||
|
* - Crafting the ink using the bank crafting preset hotkey on Minus
|
||||||
|
*
|
||||||
|
* @param volume The number of necromancy ink to craft
|
||||||
|
* @param agent The orchestrator instance to use, defaults to global instance
|
||||||
|
* @param bankPoint The tile position of the bank booth to use
|
||||||
|
*/
|
||||||
fun createNecromancyInk(volume: Int, agent: RSOrchestrator = RSOrchestrator.getInstance(), bankPoint: Point = agent.getBankPoint()) {
|
fun createNecromancyInk(volume: Int, agent: RSOrchestrator = RSOrchestrator.getInstance(), bankPoint: Point = agent.getBankPoint()) {
|
||||||
val params = StandingTaskParams(
|
val params = StandingTaskParams(
|
||||||
volume,
|
volume,
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import java.awt.Point
|
|||||||
*
|
*
|
||||||
* @param totalVolume Total number of items to process.
|
* @param totalVolume Total number of items to process.
|
||||||
* @param volumePerStep The volume of items to process per iteration.
|
* @param volumePerStep The volume of items to process per iteration.
|
||||||
* @param agent The Agent instance.
|
|
||||||
* @param bankPoint Location of the bank.
|
* @param bankPoint Location of the bank.
|
||||||
* @param bankPresetHotkey Bank preset hotkey to use.
|
* @param bankPresetHotkey Bank preset hotkey to use.
|
||||||
* @param craftingDialogHotkey Hotkey to open crafting dialog.
|
* @param craftingDialogHotkey Hotkey to open crafting dialog.
|
||||||
|
|||||||
@ -56,7 +56,15 @@ object HelperFunctions {
|
|||||||
*/
|
*/
|
||||||
fun report(step: Int, of: Int, dur: Long) {
|
fun report(step: Int, of: Int, dur: Long) {
|
||||||
val remaining = (dur / step) * (of - step)
|
val remaining = (dur / step) * (of - step)
|
||||||
print("\rStep $step of $of (${prettyTimeString(dur)} complete\t|\t~${prettyTimeString(remaining)} remaining) ")
|
if (step == 0) {
|
||||||
|
print("\rGathering timing data...\t|\tNo current ETA...) ")
|
||||||
|
} else if (step < 8) { // The time estimation is terrible, so it converges on reality. it takes roughly 8-10 steps to get a decent picture
|
||||||
|
print("\rStep $step of $of (${prettyTimeString(dur)} complete\t|\t~${prettyTimeString(remaining * 2)} remaining [LOW CONFIDENCE]) ")
|
||||||
|
} else if (step == of) {
|
||||||
|
print("\rFinal step (${prettyTimeString(dur)} complete\t|\t~${prettyTimeString(dur / (of - 1))} remaining) ")
|
||||||
|
} else {
|
||||||
|
print("\rStep $step of $of (${prettyTimeString(dur)} complete\t|\t~${prettyTimeString(remaining)} remaining) ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,9 +84,6 @@ object HelperFunctions {
|
|||||||
* @return A string representation of the duration, in the format XhYmZs
|
* @return A string representation of the duration, in the format XhYmZs
|
||||||
*/
|
*/
|
||||||
fun prettyTimeString(durationMillis: Long): String {
|
fun prettyTimeString(durationMillis: Long): String {
|
||||||
if (durationMillis == 0L) {
|
|
||||||
return "No time data yet"
|
|
||||||
}
|
|
||||||
val millisPerSecond = 1000L
|
val millisPerSecond = 1000L
|
||||||
val millisPerMinute = 60L * millisPerSecond
|
val millisPerMinute = 60L * millisPerSecond
|
||||||
val millisPerHour = 60L * millisPerMinute
|
val millisPerHour = 60L * millisPerMinute
|
||||||
|
|||||||
@ -7,7 +7,7 @@ class OSProxyTest {
|
|||||||
@Test
|
@Test
|
||||||
fun test(){
|
fun test(){
|
||||||
val vc = ConcreteVisionController()
|
val vc = ConcreteVisionController()
|
||||||
val rect = vc.getForegroundWindowBounds()
|
val rect = vc.getScaledForegroundWindowBounds()
|
||||||
println(rect)
|
println(rect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7,6 +7,6 @@ class VisionControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun testImageCapture(){
|
fun testImageCapture(){
|
||||||
val vc = ConcreteVisionController()
|
val vc = ConcreteVisionController()
|
||||||
vc.takeScreenshot()
|
vc.takeScreenshotOfForeground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user