Example usage: {@code controller().keyboard().sendKeyRelease(KeyEvent.VK_SHIFT);} + * + * @param javaKeyCode the {@link KeyEvent} key to hold */ - private void prepareInput() { - BaseScript.checkInterrupted(); - StateManager.setState(BotState.ACTING); - StatisticsManager.incrementInputs(); + public void sendKeyDown(int javaKeyCode) { + prepareInput(); + if (!input.isKeyHeld(javaKeyCode)) { + input.holdKey(javaKeyCode); + } } /** - * Sends a keyboard event for a modifier key (e.g., Shift, Ctrl, Alt, Enter). + * Releases a key, given that it is held. As this function requires an int keycode, please use + * {@link KeyEvent}. Typing {@code KeyEvent.VK_} should show contextual actions in your IDE of + * choice, showing you available keys which are mapped to integer keycodes. You may opt to look + * for Java VK keycodes online, however this is the best approach. * - * @param eventId 401 to simulate a key press, or 402 to simulate a key release. - * @param key The name of the modifier key. Acceptable values: "shift", "enter", "alt", "ctrl", - * "esc", "space". - * @throws IllegalArgumentException if the key name is invalid. + *
Example usage: {@code controller().keyboard().sendKeyRelease(KeyEvent.VK_SHIFT);} + * + * @param javaKeyCode the {@link KeyEvent} key to release */ - public synchronized void sendModifierKey(int eventId, String key) { + public void sendKeyRelease(int javaKeyCode) { prepareInput(); - int keyId = - switch (key.toLowerCase()) { - case "shift" -> 16; - case "enter" -> 10; - case "alt" -> 18; - case "ctrl" -> 17; - case "esc" -> 27; - case "space" -> 32; - default -> throw new IllegalArgumentException("Invalid modifier key: " + key); - }; - kinput.sendVirtualKeyEvent(eventId, keyId, key); + if (input.isKeyHeld(javaKeyCode)) { + input.releaseKey(javaKeyCode); + } + } + + /** + * Checks whether a key is currently being held. + * + * @param javaKeyCode the {@link KeyEvent} key to check + * @return Whether the key is currently being held or not + */ + public boolean isKeyHeld(int javaKeyCode) { + return input.isKeyHeld(javaKeyCode); } /** - * Sends a function key (F1-F12) using the KeyCode slot. + * Types out a given string to the client window using heuristics to mimic a human. Uses default + * heuristic settings for convenience. * - * @param eventId 401 to simulate a key press, or 402 to simulate a key release. - * @param functionKeyNumber The number of the F key (e.g., 6 for F6 or 1 for F1). + * @param string The String of characters to type out in a human like fashion */ - public synchronized void sendFunctionKey(int eventId, int functionKeyNumber) { + public synchronized void sendString(String string) { prepareInput(); - int keyId = 111 + functionKeyNumber; // F1 starts at 112 - kinput.sendVirtualKeyEvent(eventId, keyId, "F" + functionKeyNumber); + for (char c : string.toCharArray()) { + int keyWait = RANDOM.nextInt(30, 60); + int keyModWait = RANDOM.nextInt(30, 60); + int keyPressWait = RANDOM.nextInt(40, 85); + input.sendString(String.valueOf(c), keyWait, keyModWait); + BaseScript.waitMillis(keyPressWait); + } } /** - * Sends a keyboard event for an arrow (directional) key. + * Types out a given string to the client window using heuristics to mimic a human. Internally + * randomises between 1x - 1.1x the given modifier value. * - * @param eventId 401 to simulate a key press, or 402 to simulate a key release. - * @param key The arrow direction. Acceptable values: "up", "down", "left", "right". - * @throws IllegalArgumentException if the arrow direction is invalid. + * @param string The String of characters to type out in a human like fashion + * @param keyWait The amount of time to hold a key down + * @param keyModWait The amount of time to hold a modifier key down (e.g., shift) + * @param keyPressWait The amount of time to wait between pressing keys */ - public synchronized void sendArrowKey(int eventId, String key) { + public synchronized void sendString( + String string, int keyWait, int keyModWait, int keyPressWait) { prepareInput(); - int keyId = - switch (key.toLowerCase()) { - case "left" -> 37; - case "up" -> 38; - case "right" -> 39; - case "down" -> 40; - default -> throw new IllegalArgumentException("Invalid arrow key: " + key); - }; - kinput.sendVirtualKeyEvent(eventId, keyId, key); + for (char c : string.toCharArray()) { + input.sendString(String.valueOf(c), keyWait, keyModWait); + BaseScript.waitMillis(keyPressWait); + } } } diff --git a/src/main/java/com/chromascape/utils/core/input/mouse/MouseOverlay.java b/src/main/java/com/chromascape/utils/core/input/mouse/MouseOverlay.java deleted file mode 100644 index 1fc371e..0000000 --- a/src/main/java/com/chromascape/utils/core/input/mouse/MouseOverlay.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.chromascape.utils.core.input.mouse; - -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Point; -import javax.swing.JFrame; - -/** - * A lightweight transparent overlay used to visually render the virtual mouse position. This - * overlay draws a red "X" at the current mouse coordinates and remains always on top, without - * interfering with user input or system focus. - */ -public class MouseOverlay extends JFrame { - - private Point mousePoint = new Point(0, 0); - - /** - * Constructs the mouse overlay window. The overlay is frameless, transparent, always on top, and - * spans the full screen. It is also hidden from the taskbar and does not steal focus. - */ - public MouseOverlay() { - setUndecorated(true); - setBackground(new Color(0, 0, 0, 0)); - setAlwaysOnTop(true); - setFocusableWindowState(false); - setType(Type.UTILITY); - - setLayout(null); - setVisible(true); - } - - /** - * Updates the overlay to draw the red "X" at the specified mouse location. - * - * @param p The new position to draw the virtual mouse indicator. - */ - public void setMousePoint(Point p) { - this.mousePoint = p; - repaint(); - } - - /** - * Paints the virtual mouse indicator at the last provided point. Draws a red "X" using two - * diagonal lines. - * - * @param g The Graphics context for the overlay window. - */ - @Override - public void paint(Graphics g) { - super.paint(g); - Graphics2D g2d = (Graphics2D) g.create(); - g2d.setColor(Color.RED); - g2d.setStroke(new BasicStroke(2f)); - int size = 6; - - // Convert from global screen space to window-local coordinates - int x = mousePoint.x - getX(); - int y = mousePoint.y - getY(); - - g2d.drawLine(x - size, y - size, x + size, y + size); // Top-left to bottom-right - g2d.drawLine(x - size, y + size, x + size, y - size); // Bottom-left to top-right - - g2d.dispose(); - } - - /** - * Erases the overlay completely, removing it from the screen and freeing resources. After calling - * this method, the overlay will no longer be visible or repaintable. - */ - public void eraseOverlay() { - // Hide the window - setVisible(false); - // Dispose of the JFrame resources - dispose(); - // Reset the mouse point just in case - mousePoint = new Point(0, 0); - } -} diff --git a/src/main/java/com/chromascape/utils/core/input/mouse/VirtualMouseUtils.java b/src/main/java/com/chromascape/utils/core/input/mouse/VirtualMouseUtils.java index 8df7f42..3889cbf 100644 --- a/src/main/java/com/chromascape/utils/core/input/mouse/VirtualMouseUtils.java +++ b/src/main/java/com/chromascape/utils/core/input/mouse/VirtualMouseUtils.java @@ -1,268 +1,280 @@ package com.chromascape.utils.core.input.mouse; import com.chromascape.base.BaseScript; -import com.chromascape.utils.core.input.remoteinput.Kinput; -import com.chromascape.utils.core.screen.window.ScreenManager; +import com.chromascape.utils.core.input.remoteinput.MouseButton; +import com.chromascape.utils.core.input.remoteinput.RemoteInput; import com.chromascape.utils.core.state.BotState; import com.chromascape.utils.core.state.StateManager; import com.chromascape.utils.core.statistics.StatisticsManager; import java.awt.Point; import java.awt.Rectangle; +import java.util.Objects; import java.util.Random; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import javax.swing.SwingUtilities; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** - * The orchestrator for human-like virtual mouse behavior. - * - *
Provides movement, clicks, overshoots, pause-corrections, and overlay visuals without - * hijacking the physical system cursor. Designed for use with remote input libraries like {@link - * Kinput}, enabling low-level mouse event simulation. - * - *
Performance Note: This class uses a Producer-Consumer threading model. The physics
- * engine (Producer) runs on the calling thread at ~60Hz, while the Input Dispatcher (Consumer) runs
- * on a background thread. This ensures that slow hardware input calls do not slow down the physics
- * calculation.
+ * High level abstraction for a user to handle mouse IO through the {@link
+ * com.chromascape.controller.Controller}. Orchestrator of both the mouse movement physics
+ * calculation and dispatching of IO via the JNA layer. Uses a producer -> consumer threading model
+ * to prevent a mouse movement from lagging if the IO fails or stutters. Provides IO capabilities
+ * such as mouse movement, clicking, and holding of {@link MouseButton}s.
*/
public class VirtualMouseUtils {
/** The current virtual mouse position. */
private Point currentPosition;
- /** Timestamp of the last overlay repaint to control frame rate. */
- private long lastOverlayUpdate = 0;
-
- /** Semi-transparent visual overlay to show virtual mouse position. */
- private final MouseOverlay overlay;
-
/** Interface for injecting low-level mouse events independently of system mouse. */
- private final Kinput kinput;
-
- /** Random number generator. */
- private final Random random;
+ private final RemoteInput input;
/** Humanised mouse movement service. */
private final WindMouse windMouse;
- /** Logger with appender to publish logs to the web UI. */
- private static final Logger logger = LogManager.getLogger(VirtualMouseUtils.class);
+ private final Random random = new Random();
- /** The latest point generated by the physics engine, waiting to be consumed by Kinput. */
+ /** The latest point generated by the physics engine, waiting to be consumed by input. */
private final AtomicReference Required because the underlying WebSocket/Connection in Kinput is not thread-safe and cannot
+ * Required because the underlying WebSocket/Connection in input is not thread-safe and cannot
* handle simultaneous write operations.
*/
- private final Object kinputLock = new Object();
+ private final Object inputLock = new Object();
/**
- * The orchestrator for all inputs mouse related. provides human like mouse movement, clicking and
- * a little overlay so you can see where it is.
+ * Constructs the VMU class. Initialises the mouse overlay. Gives the mouse cursor a random start
+ * position. Starts the input consumer thread.
*
- * @param kinput The operating system dependant utility to send low level mouse inputs. This is
- * how we can still use the system cursor separately while this mouse is active.
- * @param bounds Rectangle, containing the screen's bounds.
+ * @param input A hardware RemoteInput capable object
*/
- public VirtualMouseUtils(final Kinput kinput, final Rectangle bounds) {
- this.kinput = kinput;
- overlay = new MouseOverlay();
- overlay.setSize(bounds.width, bounds.height);
-
- random = new Random();
+ public VirtualMouseUtils(RemoteInput input) {
+ this.input = input;
windMouse = new WindMouse();
-
// Randomize starting position within the client window
- int startX = bounds.x + random.nextInt(bounds.width);
- int startY = bounds.y + random.nextInt(bounds.height);
- currentPosition = new Point(startX, startY);
+ randomiseStartPos();
// Initialize atomic reference to prevent null pointer in consumer
pendingInputPoint.set(currentPosition);
-
- // Place overlay at the same randomized position
- SwingUtilities.invokeLater(() -> overlay.setMousePoint(currentPosition));
- overlay.setLocation(bounds.x, bounds.y); // keep overlay frame aligned with window
-
// Start the background Input Consumer thread
+ startInputConsumerThread();
+ }
+
+ /** Starts the input consumer thread to prepare for IO. */
+ private void startInputConsumerThread() {
Thread inputConsumerThread = new Thread(this::consumeInputLoop, "VirtualMouse-Input-Consumer");
inputConsumerThread.setDaemon(true); // Ensure thread dies when JVM shuts down
inputConsumerThread.start();
}
/**
- * The loop for the background thread. Continuously checks for new points and sends them to
- * Kinput.
+ * Randomises the start position of the cursor within the client's bounds. Only used at startup.
+ * If the bot ran before and has a persisting cursor, it will default to that instead.
+ */
+ private void randomiseStartPos() {
+ if (Objects.equals(input.getMousePosition(), new Point(0, 0))
+ || input.getMousePosition() == null) {
+
+ Rectangle bounds = input.getTargetDimensions();
+ int startX = bounds.x + random.nextInt(bounds.width);
+ int startY = bounds.y + random.nextInt(bounds.height);
+ currentPosition = new Point(startX, startY);
+ } else {
+ currentPosition = input.getMousePosition();
+ }
+ currentPosition = input.getMousePosition();
+ // Initialize atomic reference to prevent null pointer in consumer
+ pendingInputPoint.set(currentPosition);
+ }
+
+ /**
+ * Used alongside the producer thread to reliably move the mouse. While the flag is set to true,
+ * consumes the latest snapshot of the mouse movement simulation and sends a synchronised call to
+ * the IO layer to move the mouse. Discards duplicates.
*/
private void consumeInputLoop() {
Point lastSentPoint = null;
while (true) {
- try {
- if (isMoving.get()) {
- Point target = pendingInputPoint.get();
-
- // Only send input if the point is new (and not null)
- if (target != null && !target.equals(lastSentPoint)) {
- Point clientPoint = ScreenManager.toClientCoords(target);
-
- // Prevent collision with clicks or final snaps
- synchronized (kinputLock) {
- kinput.moveMouse(clientPoint.x, clientPoint.y);
- }
-
- lastSentPoint = target;
+ if (isMoving.get()) {
+ // Practically final value, will not change during execution
+ Point target = pendingInputPoint.get();
+
+ // Only send input if the point is new
+ if (target != null && !target.equals(lastSentPoint)) {
+ synchronized (inputLock) {
+ input.moveMouse(target);
}
- } else {
- // If not moving, sleep longer to save resources
- BaseScript.waitMillis(5);
+ lastSentPoint = target;
}
- } catch (Exception e) {
- // Log error but keep thread alive
- logger.error("Error while consuming inputL {}", e.getMessage());
- logger.error(e.getStackTrace());
+ } else {
+ // If not moving, sleep longer to save resources
+ BaseScript.waitMillis(5);
}
}
}
/**
- * Moves the virtual mouse to the given location using the WindMouse algorithm.
- *
- * @param target The destination point.
- * @param speed Speed profile: "slow", "medium", "fast".
+ * Intended to be called before IO execution. Checks whether the script has been stopped to
+ * reliably prevent execution. Sets the bot state to acting and increments inputs for the web UI's
+ * statistics.
*/
- public void moveTo(final Point target, final String speed) {
+ private void prepareInput() {
BaseScript.checkInterrupted();
StateManager.setState(BotState.ACTING);
StatisticsManager.incrementInputs();
+ }
+
+ /**
+ * Moves the mouse to a target destination point onscreen at a given speed using a humanised mouse
+ * movement algorithm.
+ *
+ * @param target The {@link Point} location to travel to
+ * @param speed How fast the mouse should travel, "slow", "medium" or "fast"
+ */
+ public void moveTo(final Point target, final String speed) {
+ prepareInput();
// Flag start of movement for the Consumer thread
isMoving.set(true);
try {
- // Run Physics Loop (Producer) on the current thread.
- // The callback purely updates the AtomicReference and Overlay.
- // It does NOT wait for Kinput.
- windMouse.move(
- currentPosition,
- target,
- speed,
- p -> {
- // Update the shared reference for the Consumer to pick up
- pendingInputPoint.set(p);
- currentPosition = p;
-
- // Throttle overlay updates to ~60 FPS (16ms)
- long now = System.currentTimeMillis();
- if (now - lastOverlayUpdate >= 16) {
- if (!SwingUtilities.isEventDispatchThread()) {
- SwingUtilities.invokeLater(() -> overlay.setMousePoint(p));
- lastOverlayUpdate = now;
- }
- }
- });
+ // WindMouse's physics loop will run on the current thread, producer
+ // The hardware IO execution (plus overlay/current state) is the consumer thread
+ windMouse.move(currentPosition, target, speed, this::moveMouseImpl);
} finally {
- // Movement finished.
- isMoving.set(false);
-
- // Force the final position update to ensure exact accuracy.
- currentPosition = target;
- pendingInputPoint.set(target); // Sync consumer for safety
+ finaliseMovement(target);
+ }
+ }
- Point finalClientPoint = ScreenManager.toClientCoords(target);
+ /**
+ * Callback for the WindMouse algorithm, where it used to execute the mouse movement action. This
+ * function only updates the state for the consumer thread, however serves as a nod to the source.
+ *
+ * @param p The point generated by WindMouse.
+ */
+ private void moveMouseImpl(Point p) {
+ pendingInputPoint.set(p);
+ currentPosition = p;
+ }
- // SYNCHRONIZED: Ensure final snap doesn't crash connection
- synchronized (kinputLock) {
- kinput.moveMouse(finalClientPoint.x, finalClientPoint.y);
- }
+ /**
+ * Disables the flag for the consumer thread, signaling there is no more work to do. Forces the
+ * destination point for the internal state, remote input, and overlay; to ensure scripts do not
+ * fail.
+ *
+ * @param target The final mouse destination.
+ */
+ private void finaliseMovement(Point target) {
+ // Movement finished.
+ isMoving.set(false);
+ // Force the final position update to ensure exact accuracy.
+ currentPosition = target;
+ pendingInputPoint.set(target);
+ // Snap to final position to force script safety
+ // synchronized to prevent race conditions with other IO
+ synchronized (inputLock) {
+ input.moveMouse(target);
+ }
+ }
- SwingUtilities.invokeLater(() -> overlay.setMousePoint(target));
+ /**
+ * Executes a mouse button press and release in a human-like fashion, given which button to press.
+ * Synchronised as not to collide with other IO.
+ *
+ * @param button the mouse button to press
+ */
+ private void click(MouseButton button) {
+ prepareInput();
+ synchronized (inputLock) {
+ input.holdMouse(button);
+ BaseScript.waitRandomMillis(50, 80);
+ input.releaseMouse(button);
}
}
- /** Simulates a left-click at the current virtual mouse location. */
+ /** Left clicks at the current mouse position. */
public void leftClick() {
- performClick(
- (p) -> {
- synchronized (kinputLock) {
- kinput.clickMouse(p.x, p.y, Kinput.MouseButton.LEFT.id);
- // Redundant move ensures OS registers click at correct coords if jitter occurred
- kinput.moveMouse(p.x, p.y);
- }
- });
+ click(MouseButton.left);
}
- /** Simulates a right-click at the current virtual mouse location. */
+ /** Right clicks at the current mouse position. */
public void rightClick() {
- performClick(
- (p) -> {
- synchronized (kinputLock) {
- kinput.clickMouse(p.x, p.y, Kinput.MouseButton.RIGHT.id);
- kinput.moveMouse(p.x, p.y);
- }
- });
+ click(MouseButton.right);
+ }
+
+ /** Middle clicks at the current mouse position. */
+ public void middleClick() {
+ click(MouseButton.middle);
}
/**
- * Simulates a middle mouse button input.
+ * Holds down a Mouse button given which button to hold.
*
- * @param eventType The event code (501 for press, 502 for release).
+ * @param button the {@link MouseButton} to hold.
*/
- public void middleClick(int eventType) {
- performClick(
- (p) -> {
- synchronized (kinputLock) {
- kinput.middleInput(p.x, p.y, eventType);
- }
- });
+ public void holdMouseButton(MouseButton button) {
+ prepareInput();
+ synchronized (inputLock) {
+ if (!input.isMouseHeld(button)) {
+ input.holdMouse(button);
+ }
+ }
}
/**
- * Applies a micro-jitter (1–3 pixels) to the virtual cursor, intended to simulate small human
- * hand movements when clicking.
+ * Queries whether a {@link MouseButton} is currently being held.
+ *
+ * @param button The {@link MouseButton} to check
+ * @return Whether it's currently being held
*/
- public void microJitter() {
- if (random.nextBoolean()) {
- currentPosition.translate(random.nextInt(-1, 2), random.nextInt(-1, 4));
-
- // Update consumer reference so hardware reflects jitter
- pendingInputPoint.set(currentPosition);
-
- SwingUtilities.invokeLater(() -> overlay.setMousePoint(currentPosition));
-
- // We can force this update directly as jitter is a single discrete action,
- // not a high-frequency loop.
- Point clientPoint = ScreenManager.toClientCoords(currentPosition);
+ public boolean isMouseButtonHeld(MouseButton button) {
+ return input.isMouseHeld(button);
+ }
- synchronized (kinputLock) {
- kinput.moveMouse(clientPoint.x, clientPoint.y);
+ /**
+ * Releases a Mouse button given which button to release.
+ *
+ * @param button the {@link MouseButton} to release.
+ */
+ public void releaseMouseButton(MouseButton button) {
+ prepareInput();
+ synchronized (inputLock) {
+ if (input.isMouseHeld(button)) {
+ input.releaseMouse(button);
}
}
}
- /** Helper to perform common action logic (Stats, State, Coordinate Conversion). */
- private void performClick(Consumer This file is part of the SMART Minimizing Autoing Resource Thing (SMART)
+ *
+ * SMART is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * SMART is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
+ * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with SMART. If not,
+ * see This class simulates mouse movement using a physics-based model that accounts for:
+ *
+ * The implementation has been tuned for standard simulation (approx. 60Hz internal logic) to
- * ensure compatibility with standard client ticking rates while maintaining the statistical
- * properties of the original algorithm.
+ * This modified impl has been tuned to 60hz as opposed to 30
*
* Original Algorithm by BenLand100. Tweaked
* "WindMouse2" implementation by holic.
- * Adapted for ChromaScape by StaticSweep.
+ * Adapted for ChromaScape.
*/
public class WindMouse {
@@ -40,18 +61,14 @@ public class WindMouse {
* @param target The destination coordinates.
* @param speedProfile A string constant determining movement characteristics ("slow", "medium",
* "fast"). Defaults to "medium" if the profile is unrecognized.
- * @param onMove A {@link Consumer} that accepts a {@link Point} for every step of the path. This
- * is typically used to trigger the actual hardware or robot input.
+ * @param moveMouseImpl A {@link Consumer} that accepts a {@link Point} for every step of the
+ * path. This is typically used to trigger the actual hardware or robot input.
*/
- public void move(Point start, Point target, String speedProfile, Consumer If the distance is greater than 250 pixels, there is a 50% chance an intermediate point will
- * be generated to simulate a human "correction arc" or two-stage movement.
+ * Algorithm by BenLand100, modified by holic and later ChromaScape.
*
* @param start The starting point.
* @param target The final destination.
* @param gravity The gravitational pull towards the target.
* @param wind The magnitude of random perturbations.
* @param speed The timing speed factor.
- * @param onMove The callback for cursor updates.
+ * @param moveMouseImpl The callback for cursor updates.
*/
private void windMouse2(
Point start,
@@ -91,10 +107,8 @@ private void windMouse2(
double gravity,
double wind,
double speed,
- Consumer This method runs a loop that continuously calculates velocity vectors based on gravity
- * (distance to target) and wind (random noise). It applies these vectors to the current position
- * and triggers the {@code onMove} callback.
- *
- * @param xs Current X position.
- * @param ys Current Y position.
- * @param xe Target X position.
- * @param ye Target Y position.
- * @param gravity The strength of the pull towards the target.
- * @param wind The strength of the random wind forces.
- * @param speed Controls the sleep duration between steps.
- * @param targetArea The radius (in pixels) around the target where "braking" logic begins.
- * @param onMove The callback to execute when the integer coordinates change.
+ * @param xs The x start
+ * @param ys The y start
+ * @param xe The x destination
+ * @param ye The y destination
+ * @param gravity Strength pulling the position towards the destination
+ * @param wind Strength pulling the position in random directions
+ * @param speed Influences the rate of sleeps, speeding up or slowing down the routine
+ * @param targetArea Radius of area around the destination that should trigger slowing, prevents
+ * spiraling
*/
private void windMouseImpl(
double xs,
@@ -151,7 +171,6 @@ private void windMouseImpl(
double dist, veloX = 0, veloY = 0, windX = 0, windY = 0;
- // Pre-calculated square roots for vector normalization
double sqrt2 = Math.sqrt(2);
double sqrt3 = Math.sqrt(3);
double sqrt5 = Math.sqrt(5);
@@ -159,29 +178,19 @@ private void windMouseImpl(
int tDist = (int) distance(new Point((int) xs, (int) ys), new Point((int) xe, (int) ye));
long t = System.currentTimeMillis() + 10000; // 10-second timeout safety
- // Loop until we are within 3 pixels.
- // Stopping at 3px prevents the "micro-orbiting" physics glitch where gravity
- // causes the cursor to overshoot and circle the target pixel indefinitely.
while ((dist = Math.hypot((xs - xe), (ys - ye))) >= 3) {
if (System.currentTimeMillis() > t) break;
- // Cap wind force so it doesn't exceed the remaining distance
wind = Math.min(wind, dist);
- // Adaptive step size based on total distance
long d = (Math.round((Math.round(((double) (tDist))) * 0.3)) / 7);
if (d > 20) d = 20;
if (d < 5) d = 5;
- // Occasional random slowdown
if (random.nextInt(6) == 0) {
d = 2;
}
- // Max step calculation.
- // Multiplier set to 1.5 (Standard WindMouse).
- // Since we run at 60Hz (half the 120Hz rate), we need to cover twice the
- // distance per frame to preserve the visual speed.
double maxStep = (Math.min(d, Math.round(dist))) * 1.5;
if (dist >= targetArea) {
@@ -190,23 +199,17 @@ private void windMouseImpl(
windX = (windX / sqrt3) + ((random.nextInt(windRange) - wind) / sqrt5);
windY = (windY / sqrt3) + ((random.nextInt(windRange) - wind) / sqrt5);
} else {
- // Short range (Entering Target Area):
- // Dampen wind (divide by sqrt2)
+
windX = (windX / sqrt2);
windY = (windY / sqrt2);
- // (Friction)
- // Multiplier 0.64 allows velocity to decay naturally over the standard frame count.
- // (Previously 0.80 at 120Hz; 0.80^2 approx 0.64 for 60Hz).
veloX *= 0.64;
veloY *= 0.64;
}
- // Apply forces to velocity
veloX += windX + gravity * (xe - xs) / dist;
veloY += windY + gravity * (ye - ys) / dist;
- // Cap velocity at maxStep
if (Math.hypot(veloX, veloY) > maxStep) {
maxStep = ((maxStep / 2) < 1) ? 2 : maxStep;
double randomDist = (maxStep / 2) + random.nextInt((int) (Math.round(maxStep) / 2));
@@ -215,21 +218,16 @@ private void windMouseImpl(
veloY = (veloY / veloMag) * randomDist;
}
- // Update position
int lastX = ((int) (Math.round(xs)));
int lastY = ((int) (Math.round(ys)));
xs += veloX;
ys += veloY;
- // Only fire callback if the integer pixel coordinate actually changed
if ((lastX != Math.round(xs)) || (lastY != Math.round(ys))) {
Point newP = new Point((int) Math.round(xs), (int) Math.round(ys));
if (onMove != null) onMove.accept(newP);
}
- // Sleep calculation
- // Logic yields ~10-20ms sleeps, targeting standard 60Hz.
- // (Multiplier increased from 6 to 12 to double sleep times).
int w = random.nextInt((int) (Math.round(100.0 / speed))) * 12;
if (w < 10) {
w = 10;
@@ -239,7 +237,6 @@ private void windMouseImpl(
sleepPrecise(w);
}
- // Instantly bridge the last <3 pixels to ensure pixel-perfect accuracy.
if ((Math.round(xe) != Math.round(xs)) || (Math.round(ye) != Math.round(ys))) {
Point finalP = new Point((int) Math.round(xe), (int) Math.round(ye));
if (onMove != null) onMove.accept(finalP);
@@ -247,31 +244,20 @@ private void windMouseImpl(
}
/**
- * Executes a high-precision sleep using a hybrid approach.
- *
- * Standard {@link Thread#sleep(long)} is too coarse (~15ms resolution on Windows) for smooth
- * mouse movement. Pure busy-waiting burns 100% CPU.
- *
- * This method parks the thread (yields CPU) for the majority of the duration, then switches to
- * a busy-wait spin loop for the final millisecond to ensure sub-millisecond precision.
+ * Precisely sleeps for a given length of time, as other approaches aren't as accurate.
*
* @param millis The duration to sleep in milliseconds.
*/
private void sleepPrecise(long millis) {
long end = System.nanoTime() + millis * 1_000_000L;
long timeLeft = end - System.nanoTime();
-
- // If we have more than 2ms, we can afford to park the thread to save CPU.
- // We wake up 1ms early to spin for the final precision.
while (timeLeft > 2_000_000L) {
parkNanos(timeLeft - 1_000_000L);
timeLeft = end - System.nanoTime();
}
- // Busy-wait for the final <1ms
while (System.nanoTime() < end) {
try {
- // Java 9+ hint to CPU to optimize power consumption during spin-waits
java.lang.Thread.onSpinWait();
} catch (NoSuchMethodError e) {
// Fallback for older JDKs (implicitly just busy-waits)
@@ -284,7 +270,7 @@ private void sleepPrecise(long millis) {
*
* @param p1 The first point.
* @param p2 The second point.
- * @return pixel perfect distance.
+ * @return The distance.
*/
private double distance(Point p1, Point p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
@@ -304,10 +290,7 @@ private Point randomPoint(Point p1, Point p2) {
}
/**
- * Generates a pseudo-random floating-point value between two bounds.
- *
- * This utility is used to calculate randomized intermediate coordinates (waypoints) when
- * generating the mouse path. It handles both positive and negative directions automatically.
+ * Generates a random floating-point value between two bounds.
*
* @param corner1 The first boundary (e.g., the starting coordinate).
* @param corner2 The second boundary (e.g., the target coordinate).
diff --git a/src/main/java/com/chromascape/utils/core/input/remoteinput/ControlKey.java b/src/main/java/com/chromascape/utils/core/input/remoteinput/ControlKey.java
new file mode 100644
index 0000000..1bc349f
--- /dev/null
+++ b/src/main/java/com/chromascape/utils/core/input/remoteinput/ControlKey.java
@@ -0,0 +1,24 @@
+package com.chromascape.utils.core.input.remoteinput;
+
+/**
+ * Key value pair enum containing keys and their Java key code. Used to define the preferred Java
+ * keycode for RemoteInput. e.g. enter is typically 10, we want 13.
+ */
+public enum ControlKey {
+ VK_LEFT_CONTROL(162),
+ VK_LEFT_ALT(164),
+ VK_RIGHT_ALT(165),
+ VK_LEFT_WINDOWS(91),
+ VK_RETURN(13);
+
+ public final int nativeCode;
+
+ /**
+ * Constructs the enum with an extra value, which contains the java keycode.
+ *
+ * @param nativeCode the Java keycode
+ */
+ ControlKey(int nativeCode) {
+ this.nativeCode = nativeCode;
+ }
+}
diff --git a/src/main/java/com/chromascape/utils/core/input/remoteinput/Kinput.java b/src/main/java/com/chromascape/utils/core/input/remoteinput/Kinput.java
deleted file mode 100644
index b0dfe8f..0000000
--- a/src/main/java/com/chromascape/utils/core/input/remoteinput/Kinput.java
+++ /dev/null
@@ -1,321 +0,0 @@
-package com.chromascape.utils.core.input.remoteinput;
-
-import com.sun.jna.Library;
-import com.sun.jna.Native;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Random;
-
-/**
- * Provides a wrapper for the native Kinput interface, allowing injection of mouse and keyboard
- * events into a target Java application's process via JNA and Kinput DLLs.
- *
- * This class is responsible for:
- *
- * Intended for use with 64-bit Java applications that expose a {@code java.awt.Canvas}.
- */
-public class Kinput {
-
- /** JNA interface mapping for KInputCtrl.dll native methods. */
- public interface KinputInterface extends Library {
-
- /**
- * Creates a Kinput instance for the specified process ID.
- *
- * @param pid the target process ID
- * @return true if creation succeeded; false otherwise
- */
- boolean KInput_Create(int pid);
-
- /**
- * Deletes the Kinput instance for the specified process ID.
- *
- * @param pid the target process ID
- * @return true if deletion succeeded; false otherwise
- */
- boolean KInput_Delete(int pid);
-
- /**
- * Sends a focus event to the target process.
- *
- * @param pid the target process ID
- * @param eventID the focus event type (e.g., gain or loss)
- * @return true if event succeeded; false otherwise
- */
- boolean KInput_FocusEvent(int pid, int eventID);
-
- /**
- * Sends a keyboard event to the target process.
- *
- * @param pid the target process ID
- * @param eventID the keyboard event ID
- * @param when event timestamp for queuing
- * @param modifiers key modifiers
- * @param keyCode virtual key code
- * @param keyChar the character associated
- * @param keyLocation key location
- * @return true if the event was successfully sent; false otherwise
- */
- boolean KInput_KeyEvent(
- int pid,
- int eventID,
- long when,
- int modifiers,
- int keyCode,
- short keyChar,
- int keyLocation);
-
- /**
- * Sends a mouse event to the target process.
- *
- * @param pid the target process ID
- * @param eventID the mouse event type ID
- * @param when event timestamp for queuing
- * @param modifiers mouse modifiers
- * @param x the x-coordinate of the mouse event
- * @param y the y-coordinate of the mouse event
- * @param clickCount number of clicks
- * @param popupTrigger whether this event should pop up
- * @param button mouse button ID
- * @return true if the event was successfully sent; false otherwise
- */
- boolean KInput_MouseEvent(
- int pid,
- int eventID,
- long when,
- int modifiers,
- int x,
- int y,
- int clickCount,
- boolean popupTrigger,
- int button);
- }
-
- private static final int FOCUS_GAINED = 1004;
- private static final int FOCUS_LOST = 1005;
-
- private final int pid;
- private final KinputInterface kinput;
-
- /** Mouse event type IDs mapped to standard AWT semantics. */
- public enum MouseEventType {
- MOUSE_CLICK(500),
- MOUSE_PRESS(501),
- MOUSE_RELEASE(502),
- MOUSE_MOVE(503),
- MOUSE_ENTER(504),
- MOUSE_EXIT(505),
- MOUSE_DRAG(506),
- MOUSE_WHEEL(507);
-
- public final int id;
-
- MouseEventType(int id) {
- this.id = id;
- }
- }
-
- /** Mouse button IDs. */
- public enum MouseButton {
- NONE(0),
- LEFT(1),
- MIDDLE(2),
- RIGHT(3);
-
- public final int id;
-
- MouseButton(int id) {
- this.id = id;
- }
- }
-
- /**
- * Constructs a new Kinput instance for the given process ID.
- *
- * @param pid the process ID of the target Java application
- */
- public Kinput(int pid) {
- this.pid = pid;
- this.kinput = loadLibrary();
- if (!kinput.KInput_Create(pid)) {
- throw new RuntimeException("Failed to create Kinput instance for PID: " + pid);
- }
- }
-
- /**
- * Loads the Kinput native libraries from the build directory and prepares them for JNA.
- *
- * @return the loaded KinputInterface
- */
- private static KinputInterface loadLibrary() {
- try {
- // Try to load from build/dist directory first (preferred)
- Path buildDistPath = Path.of("build/dist");
- Path dllFileCtrl = buildDistPath.resolve("KInputCtrl.dll");
- Path dllFile64 = buildDistPath.resolve("KInput.dll");
-
- if (Files.exists(dllFileCtrl) && Files.exists(dllFile64)) {
- // Use build directory directly
- System.setProperty("jna.library.path", buildDistPath.toAbsolutePath().toString());
- return Native.load("KInputCtrl", KinputInterface.class);
- }
-
- // If DLLs are not found in build directory, throw an error
- throw new IOException(
- "Missing native libraries in build/dist directory. Please run the build process first.");
- } catch (Throwable e) {
- throw new RuntimeException("Failed to load Kinput DLLs", e);
- }
- }
-
- /** Sleeps briefly to simulate human-like click timing. */
- private void sleepHumanClick() {
- try {
- Thread.sleep(new Random().nextInt(50, 80));
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
-
- /** Ensures the target window is focused before sending input. */
- private synchronized void focus() {
- if (!kinput.KInput_FocusEvent(pid, FOCUS_GAINED)) {
- throw new RuntimeException("Focus event failed for PID: " + pid);
- }
- }
-
- /**
- * Sends a mouse press and release to a specified screen co-ordinate. This operates in client
- * relative co-ordinates, not screen relative co-ordinates.
- *
- * @param x the x co-ordinate of the action.
- * @param y the y co-ordinate of the action.
- * @param button which {@link MouseButton} to use.
- */
- public synchronized void clickMouse(int x, int y, int button) {
- focus();
- if (!kinput.KInput_MouseEvent(
- pid,
- MouseEventType.MOUSE_PRESS.id,
- System.currentTimeMillis(),
- 1,
- x,
- y,
- 1,
- false,
- button)) {
- throw new RuntimeException(button + " press failed");
- }
- sleepHumanClick();
- if (!kinput.KInput_MouseEvent(
- pid,
- MouseEventType.MOUSE_RELEASE.id,
- System.currentTimeMillis(),
- 1,
- x,
- y,
- 1,
- false,
- button)) {
- throw new RuntimeException(button + " release failed");
- }
- }
-
- /** Sends a middle mouse button input. */
- public synchronized void middleInput(int x, int y, int eventID) {
- focus();
- if (!kinput.KInput_MouseEvent(
- pid, eventID, System.currentTimeMillis(), 1, x, y, 1, false, MouseButton.MIDDLE.id)) {
- throw new RuntimeException("Middle mouse event failed");
- }
- }
-
- /** Moves the mouse cursor to a given screen coordinate. */
- public synchronized void moveMouse(int x, int y) {
- focus();
- if (!kinput.KInput_MouseEvent(
- pid,
- MouseEventType.MOUSE_ENTER.id,
- System.currentTimeMillis(),
- 0,
- x,
- y,
- 0,
- false,
- MouseButton.NONE.id)) {
- throw new RuntimeException("Mouse enter failed");
- }
- if (!kinput.KInput_MouseEvent(
- pid,
- MouseEventType.MOUSE_MOVE.id,
- System.currentTimeMillis(),
- 0,
- x,
- y,
- 0,
- false,
- MouseButton.NONE.id)) {
- throw new RuntimeException("Mouse move failed");
- }
- }
-
- /**
- * Sends a key character to the client window.
- *
- * @param eventID - 401 to press/ 402 to release.
- * @param keyChar - The key character to send.
- */
- public synchronized void sendKeyEvent(int eventID, char keyChar) {
- focus();
- if (!kinput.KInput_KeyEvent(
- pid, eventID, System.currentTimeMillis(), 0, 0, (short) keyChar, 0)) {
- throw new RuntimeException("Key event failed for char: " + keyChar);
- }
- }
-
- /**
- * Types a character to the client window.
- *
- * @param keyChar The character to send.
- */
- public synchronized void sendCharEvent(char keyChar) {
- focus();
- // Passes keyChar into the 'short' parameter (arg5)
- if (!kinput.KInput_KeyEvent(pid, 400, System.currentTimeMillis(), 0, 0, (short) keyChar, 0)) {
- throw new RuntimeException("Character event failed for char: '" + keyChar + "'");
- }
- }
-
- /**
- * Sends a virtual key event (Arrow, Modifier, F-Key) to the client window's SunAwtCanvas.
- *
- * @param eventID 401 for press, 402 for release.
- * @param keyCode The virtual key code.
- * @param keyName A descriptive name used purely for error logging.
- */
- public synchronized void sendVirtualKeyEvent(int eventID, int keyCode, String keyName) {
- focus();
- // Passes keyCode into the integer parameter (arg4)
- if (!kinput.KInput_KeyEvent(
- pid, eventID, System.currentTimeMillis(), 0, keyCode, (short) 0, 0)) {
- throw new RuntimeException("Virtual key event failed for: " + keyName);
- }
- }
-
- /**
- * Destroys the active KInput instance. Should be called on shutdown to free memory and close
- * resources.
- */
- public synchronized void destroy() {
- if (!kinput.KInput_Delete(pid)) {
- throw new RuntimeException("Failed to delete KInput instance");
- }
- }
-}
diff --git a/src/main/java/com/chromascape/utils/core/input/remoteinput/MouseButton.java b/src/main/java/com/chromascape/utils/core/input/remoteinput/MouseButton.java
new file mode 100644
index 0000000..e62b0fa
--- /dev/null
+++ b/src/main/java/com/chromascape/utils/core/input/remoteinput/MouseButton.java
@@ -0,0 +1,13 @@
+package com.chromascape.utils.core.input.remoteinput;
+
+/**
+ * Enum containing the available mouse buttons for use with RemoteInput. Namely, the functions
+ * {@link RemoteInput#holdMouse(MouseButton)}, {@link RemoteInput#releaseMouse(MouseButton)}, and
+ * {@link RemoteInput#isMouseHeld(MouseButton)}. The ordinal of each mouse button refers to the
+ * integer value required by RI.
+ */
+public enum MouseButton {
+ right,
+ left,
+ middle
+}
diff --git a/src/main/java/com/chromascape/utils/core/input/remoteinput/RemoteInput.java b/src/main/java/com/chromascape/utils/core/input/remoteinput/RemoteInput.java
new file mode 100644
index 0000000..b682edd
--- /dev/null
+++ b/src/main/java/com/chromascape/utils/core/input/remoteinput/RemoteInput.java
@@ -0,0 +1,333 @@
+package com.chromascape.utils.core.input.remoteinput;
+
+import com.sun.jna.Native;
+import com.sun.jna.Pointer;
+import com.sun.jna.ptr.IntByReference;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.KeyEvent;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * RemoteInput allows ChromaScape to send Java AWT signals to a target Java app, to simulate IO.
+ * This approach as opposed to sending OS signals, allows the user to fully minimise and or cover
+ * the target app. It also allows the user to keep using their computer as they wish, as if
+ * ChromaScape was never running. This class provides functionality to load a RemoteInput binary
+ * regardless of operating system, provide IO to the target application, and receive the most
+ * up-to-date snapshot of the application's Java canvas (updated whenever they draw a new frame).
+ */
+public class RemoteInput implements AutoCloseable {
+
+ private static final String COMPILED_BINARY_FILENAME = "libRemoteInput" + getExtension();
+
+ private final int pid;
+
+ /** JNA needs to load the binary as an interface, the interface acts as the exported headers. */
+ private final RemoteInputInterface remoteInput;
+
+ /**
+ * RemoteInput returns a Pointer which acts as a reference to a specific client/target. A single
+ * instance of RI can support several targets. RI requests this pointer when performing IO, to
+ * specify which target to use.
+ */
+ private Pointer target;
+
+ /**
+ * Constructs the RemoteInput class.
+ *
+ * @param pid The process ID of the target Java application
+ */
+ public RemoteInput(int pid) {
+ this.pid = pid;
+ this.remoteInput = loadRemoteInput();
+ initialise();
+ }
+
+ /**
+ * The RI binary can be compiled on Linux, Mac and Windows. This function detects OS and applies
+ * the corresponding filetype.
+ *
+ * @return OS specific filetype
+ */
+ private static String getExtension() {
+ String os = System.getProperty("os.name").toLowerCase();
+ if (os.contains("win")) {
+ return ".dll";
+ }
+ if (os.contains("mac")) {
+ return ".dylib";
+ }
+ return ".so";
+ }
+
+ /**
+ * RemoteInput expects specific Java keycodes for several keys, compared to generic keycodes.
+ * e.g., enter is typically 10, we want 13.
+ *
+ * @param javaKeyCode The {@link KeyEvent} Java keycode
+ * @return RemoteInput's preferred keycode
+ */
+ private int toNativeCode(int javaKeyCode) {
+ return switch (javaKeyCode) {
+ case KeyEvent.VK_ENTER -> ControlKey.VK_RETURN.nativeCode;
+ case KeyEvent.VK_CONTROL -> ControlKey.VK_LEFT_CONTROL.nativeCode;
+ case KeyEvent.VK_ALT -> ControlKey.VK_LEFT_ALT.nativeCode;
+ case KeyEvent.VK_ALT_GRAPH -> ControlKey.VK_RIGHT_ALT.nativeCode;
+ case KeyEvent.VK_WINDOWS -> ControlKey.VK_LEFT_WINDOWS.nativeCode;
+ default -> javaKeyCode;
+ };
+ }
+
+ /**
+ * Checks if the target application is already a registered client. If not, will inject RI into
+ * the application. Finally, it pairs with the target
+ */
+ private void initialise() {
+ if (!isInjectedClient()) {
+ remoteInput.EIOS_Inject_PID(pid);
+ }
+ pairClient();
+ }
+
+ /**
+ * Checks if the target application is already registered within RI's internal list of connected
+ * clients. A registered client is one that already has RI injected into it.
+ *
+ * @return Whether the target application is registered
+ */
+ private boolean isInjectedClient() {
+ long totalClients = remoteInput.EIOS_GetClients(false);
+
+ for (long i = 0; i < totalClients; i++) {
+ if (remoteInput.EIOS_GetClientPID(i) == pid) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Grants IO operation over a registered client by returning a pointer to reference a specific
+ * client.
+ *
+ * @see #target <- The returned pointer
+ */
+ private void pairClient() {
+ target = remoteInput.EIOS_RequestTarget(String.valueOf(pid));
+ if (target == null) {
+ throw new RuntimeException("Target Not Found with pid: " + pid);
+ }
+ }
+
+ /**
+ * Loads the RemoteInput binary as a {@link RemoteInputInterface} object to allow Java to
+ * communicate directly to the native binary. Will first check if a user compiled binary exists,
+ * if not, uses a provided pre-compiled binary.
+ *
+ * @return An interface that acts as a bridge to talk to the binary in Java, used within this
+ * class to provide IO operations
+ */
+ private static RemoteInputInterface loadRemoteInput() {
+ Path binaryFile =
+ Paths.get("third-party", "RemoteInput", "cmake-build-release", COMPILED_BINARY_FILENAME);
+ if (!Files.exists(binaryFile)) {
+ binaryFile = Paths.get("third-party", "RemoteInput", "precompiled", COMPILED_BINARY_FILENAME);
+ }
+
+ try {
+ return Native.load(binaryFile.toString(), RemoteInputInterface.class);
+ } catch (UnsatisfiedLinkError e) {
+ throw new RuntimeException("Unable to load RemoteInput binary from path", e);
+ }
+ }
+
+ /**
+ * Retrieves the memory pointer to the target's current image buffer. The buffer contains pixel
+ * data in BGRA (Blue, Green, Red, Alpha) format. Each pixel occupies 4 bytes. The total size of
+ * the buffer is {@code width * height * 4} bytes. The data is updated automatically by the native
+ * hooks in the target application whenever a frame is rendered.
+ *
+ * @return A pointer to the start of the BGRA pixel array
+ */
+ public synchronized Pointer getImageBuffer() {
+ Pointer p = remoteInput.EIOS_GetImageBuffer(target);
+ if (p == null) {
+ throw new RuntimeException("Image Buffer Not Found with pid: " + pid);
+ }
+ return p;
+ }
+
+ /**
+ * Retrieves a memory pointer similarly to {@link #getImageBuffer()}. This method however will
+ * contain the mouse pointer and any objects drawn onto the canvas.
+ *
+ * @return A pointer to the start of the BGRA pixel array
+ */
+ public synchronized Pointer getDebugImageBuffer() {
+ Pointer p = remoteInput.EIOS_GetDebugImageBuffer(target);
+ if (p == null) {
+ throw new RuntimeException("Image Buffer Not Found with pid: " + pid);
+ }
+ return p;
+ }
+
+ /** Will get focus of the client if in an unfocused state, necessary for mouse input. */
+ private void getFocusIfNotFocused() {
+ if (!remoteInput.EIOS_HasFocus(target)) {
+ remoteInput.EIOS_GainFocus(target);
+ }
+ }
+
+ /** Will enable keyboard input if it's currently disabled, necessary for keyboard input. */
+ private void setKeyboardInputIfDisabled() {
+ if (!remoteInput.EIOS_IsKeyboardInputEnabled(target)) {
+ remoteInput.EIOS_SetKeyboardInputEnabled(target, true);
+ }
+ }
+
+ /**
+ * RemoteInput stores an internal mouse position that stays persistent after ChromaScape restarts.
+ * This will return that mouse position. VirtualMouseUtils automatically syncs to this unless
+ * RemoteInput is pairing for the first time, where it'll randomise mouse position.
+ *
+ * @return A {@link Point} referring to the client relative mouse position
+ */
+ public synchronized Point getMousePosition() {
+ IntByReference x = new IntByReference();
+ IntByReference y = new IntByReference();
+ remoteInput.EIOS_GetMousePosition(target, x, y);
+ return new Point(x.getValue(), y.getValue());
+ }
+
+ /**
+ * Gets the target app's window dimensions. Due to all IO being performed in client relative
+ * space, the user can assume the origin as 0,0.
+ *
+ * @return A rectangle defining the origin and bounds of the target application
+ */
+ public synchronized Rectangle getTargetDimensions() {
+ IntByReference x = new IntByReference();
+ IntByReference y = new IntByReference();
+ remoteInput.EIOS_GetTargetDimensions(target, x, y);
+ return new Rectangle(0, 0, x.getValue(), y.getValue());
+ }
+
+ /**
+ * Sends a key down in the target java app. To get the keycode, use a {@link
+ * java.awt.event.KeyEvent} object. Example: {@code KeyEvent.VK_ENTER}.
+ *
+ * @param javaKeyCode The Java keycode corresponding to the key being pressed
+ */
+ public synchronized void holdKey(int javaKeyCode) {
+ setKeyboardInputIfDisabled();
+ getFocusIfNotFocused();
+ remoteInput.EIOS_HoldKey(target, toNativeCode(javaKeyCode));
+ }
+
+ /**
+ * Queries whether a key is currently being held.
+ *
+ * @param javaKeyCode The Java keycode of the key in question
+ * @return Whether the key is currently being held
+ */
+ public synchronized boolean isKeyHeld(int javaKeyCode) {
+ return remoteInput.EIOS_IsKeyHeld(target, toNativeCode(javaKeyCode));
+ }
+
+ /**
+ * Sends a key release event to the target Java app. This should be used in conjunction with
+ * {@link #holdKey(int)} to simulate a full key press.
+ *
+ * @param javaKeyCode The Java keycode corresponding to the key being released
+ */
+ public synchronized void releaseKey(int javaKeyCode) {
+ setKeyboardInputIfDisabled();
+ getFocusIfNotFocused();
+ remoteInput.EIOS_ReleaseKey(target, toNativeCode(javaKeyCode));
+ }
+
+ /**
+ * Sends a mouse button down in the target Java app.
+ *
+ * @param button The {@link MouseButton} button to hold
+ */
+ public synchronized void holdMouse(MouseButton button) {
+ getFocusIfNotFocused();
+ Point mousePosition = getMousePosition();
+ remoteInput.EIOS_HoldMouse(target, mousePosition.x, mousePosition.y, button.ordinal());
+ }
+
+ /**
+ * Queries whether a {@link MouseButton} is currently held.
+ *
+ * @param button The {@link MouseButton} to check
+ * @return If the mouse button is currently being held
+ */
+ public synchronized boolean isMouseHeld(MouseButton button) {
+ return remoteInput.EIOS_IsMouseHeld(target, button.ordinal());
+ }
+
+ /**
+ * Releases a mouse button at the designated client local co-ordinates.
+ *
+ * @param button The {@link MouseButton} to release
+ */
+ public synchronized void releaseMouse(MouseButton button) {
+ getFocusIfNotFocused();
+ Point mousePosition = getMousePosition();
+ remoteInput.EIOS_ReleaseMouse(target, mousePosition.x, mousePosition.y, button.ordinal());
+ }
+
+ /**
+ * Moves a mouse to a designated client local co-ordinate.
+ *
+ * @param location The {@link Point} location to snap the mouse to
+ */
+ public synchronized void moveMouse(Point location) {
+ getFocusIfNotFocused();
+ remoteInput.EIOS_MoveMouse(target, location.x, location.y);
+ }
+
+ /**
+ * Performs a mouse wheel scroll at the current virtual mouse position. RemoteInput natively adds
+ * a small float value to this, to simulate human imperfection
+ *
+ * @param notches The number of mouse notches to scroll, down is positive, up is negative
+ */
+ public synchronized void scrollMouse(int notches) {
+ getFocusIfNotFocused();
+ Point mousePosition = getMousePosition();
+ remoteInput.EIOS_ScrollMouse(target, mousePosition.x, mousePosition.y, notches);
+ }
+
+ /**
+ * Types out a string of characters whilst compensating for the need of modifier keys. Useful when
+ * typing something to a dialogue box, will compensate for special characters, however lacks delay
+ * between keypresses.
+ *
+ * @param string The text to be typed
+ * @param keyWait The time in milliseconds to hold down a key
+ * @param keyModWait The time in milliseconds to hold down modifier keys
+ */
+ public synchronized void sendString(String string, int keyWait, int keyModWait) {
+ setKeyboardInputIfDisabled();
+ getFocusIfNotFocused();
+ remoteInput.EIOS_SendString(target, string, keyWait, keyModWait);
+ }
+
+ /**
+ * Since this class implements the {@link AutoCloseable} interface, it must be closed to relieve
+ * native memory. This method will release the target, effectively shutting down RemoteInput for
+ * the particular ChromaScape instance. However, this does not delete the injected part of RI in
+ * the target, simply shuts down control over it.
+ */
+ @Override
+ public void close() {
+ if (target != null) {
+ remoteInput.EIOS_ReleaseTarget(target);
+ target = null;
+ }
+ }
+}
diff --git a/src/main/java/com/chromascape/utils/core/input/remoteinput/RemoteInputInterface.java b/src/main/java/com/chromascape/utils/core/input/remoteinput/RemoteInputInterface.java
new file mode 100644
index 0000000..7cff1bc
--- /dev/null
+++ b/src/main/java/com/chromascape/utils/core/input/remoteinput/RemoteInputInterface.java
@@ -0,0 +1,226 @@
+package com.chromascape.utils.core.input.remoteinput;
+
+import com.sun.jna.Library;
+import com.sun.jna.Pointer;
+import com.sun.jna.ptr.IntByReference;
+
+/**
+ * JNA interface to load the RemoteInput DLL as a {@link RemoteInput} object. Contains the available
+ * exported headers available for ChromaScape to use.
+ */
+public interface RemoteInputInterface extends Library {
+
+ /**
+ * Injects part of the RemoteInput Process into the target Java app. RI will only work after this
+ * pairing process has been performed.
+ *
+ * @param pid The OS process ID of which to inject into.
+ */
+ void EIOS_Inject_PID(int pid);
+
+ /**
+ * Requests to control a specific target app after it's been registered using {@link
+ * #EIOS_Inject_PID(int)}. Many targets can be controlled using a single instance of RemoteInput.
+ *
+ * @param initargs The String value of the target Java app's process ID (PID)
+ * @return A pointer reference to the target's EIOS object. This object is used in IO operations
+ * as a reference to which target should receive IO
+ */
+ Pointer EIOS_RequestTarget(String initargs);
+
+ /**
+ * Retrieves the memory pointer to the target's current image buffer. The buffer contains pixel
+ * data in BGRA (Blue, Green, Red, Alpha) format. Each pixel occupies 4 bytes. The total size of
+ * the buffer is {@code width * height * 4} bytes. The data is updated automatically by the native
+ * hooks in the target application whenever a frame is rendered.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @return A pointer to the start of the BGRA pixel array
+ */
+ Pointer EIOS_GetImageBuffer(Pointer eios);
+
+ /**
+ * Retrieves a memory pointer similarly to {@link #EIOS_GetImageBuffer(Pointer)}. This method
+ * however will contain the mouse pointer and any objects drawn onto the canvas.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @return A pointer to the start of the BGRA pixel array
+ */
+ Pointer EIOS_GetDebugImageBuffer(Pointer eios);
+
+ /**
+ * Checks if the target application has keyboard input enabled. This is useful when using {@link
+ * #EIOS_HoldKey(Pointer, int)}, {@link #EIOS_ReleaseKey(Pointer, int)} and or {@link
+ * #EIOS_SendString(Pointer, String, int, int)}.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @return If the target has keyboard input enabled
+ */
+ boolean EIOS_IsKeyboardInputEnabled(Pointer eios);
+
+ /**
+ * Sets the keyboard input to either enabled or disabled, useful when conducting keyboard input.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @param enabled Whether you want keyboard input to be enabled
+ */
+ void EIOS_SetKeyboardInputEnabled(Pointer eios, boolean enabled);
+
+ /**
+ * Gets the total number of clients that currently have RemoteInput injected into them.
+ *
+ * @param unpaired_only Whether to count only unpaired targets.
+ * @return The total number of connected targets.
+ */
+ long EIOS_GetClients(boolean unpaired_only);
+
+ /**
+ * Fetches the process ID of a client given the index of the client in RemoteInput's internal
+ * client array.
+ *
+ * @param index The index position of the client
+ * @return The process ID
+ */
+ int EIOS_GetClientPID(long index);
+
+ /**
+ * Whether the host machine has focus over the target Java app. Focus is required when sending
+ * inputs to the client, it's recommended to use {@link #EIOS_GainFocus(Pointer)} to do so.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @return Whether the host has focus over the client
+ */
+ boolean EIOS_HasFocus(Pointer eios);
+
+ /**
+ * Used to gain focus over the target app.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ */
+ void EIOS_GainFocus(Pointer eios);
+
+ /**
+ * Used to lose focus over the target app, might be useful for antiban possibly.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ */
+ void EIOS_LoseFocus(Pointer eios);
+
+ /**
+ * RemoteInput stores an internal mouse position that stays persistent after ChromaScape restarts.
+ * This will return that mouse position. VirtualMouseUtils automatically syncs to this unless
+ * RemoteInput is pairing for the first time, where it'll randomise mouse position.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @param x An {@link IntByReference} which gets mutated with the x value
+ * @param y An {@link IntByReference} which gets mutated with the y value
+ */
+ void EIOS_GetMousePosition(Pointer eios, IntByReference x, IntByReference y);
+
+ /**
+ * Gets the target app's window dimensions. Due to all IO being performed in client relative
+ * space, the user can assume the origin as 0,0.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @param width An {@link IntByReference} which gets mutated with the width's value in pixels
+ * @param height An {@link IntByReference} which gets mutated with the height's value in pixels
+ */
+ void EIOS_GetTargetDimensions(Pointer eios, IntByReference width, IntByReference height);
+
+ /**
+ * Sends a key down in the target java app. To get the keycode, use a {@link
+ * java.awt.event.KeyEvent} object. Example: {@code KeyEvent.VK_ENTER}.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @param key The Java keycode corresponding to the key being pressed
+ */
+ void EIOS_HoldKey(Pointer eios, int key);
+
+ /**
+ * Sends a mouse button down in the target java app.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @param x The x co-ordinate of the input
+ * @param y The y co-ordinate of the input
+ * @param button The {@link MouseButton} to use
+ */
+ void EIOS_HoldMouse(Pointer eios, int x, int y, int button);
+
+ /**
+ * Queries whether a key is currently held.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @param key The Java keycode of the key
+ * @return If the key is currently being held
+ */
+ boolean EIOS_IsKeyHeld(Pointer eios, int key);
+
+ /**
+ * Queries whether a {@link MouseButton} is currently held.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @param button The {@link MouseButton} to check
+ * @return If the mouse button is currently being held
+ */
+ boolean EIOS_IsMouseHeld(Pointer eios, int button);
+
+ /**
+ * Moves a mouse to a designated client local co-ordinate.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @param x The x co-ordinate of the input
+ * @param y The y co-ordinate of the input
+ */
+ void EIOS_MoveMouse(Pointer eios, int x, int y);
+
+ /**
+ * Sends a key release event to the target Java app. This should be used in conjunction with
+ * {@link #EIOS_HoldKey(Pointer, int)} to simulate a full key press.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @param key The Java keycode corresponding to the key being released
+ */
+ void EIOS_ReleaseKey(Pointer eios, int key);
+
+ /**
+ * Releases a mouse button at the designated client local co-ordinates.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @param x The x co-ordinate of the input
+ * @param y The y co-ordinate of the input
+ * @param button The {@link MouseButton} to release
+ */
+ void EIOS_ReleaseMouse(Pointer eios, int x, int y, int button);
+
+ /**
+ * Severs the connection to the target EIOS object and releases native resources. This should be
+ * called when the script stops or the controller shuts down to avoid memory leaks in the target
+ * app.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ */
+ void EIOS_ReleaseTarget(Pointer eios);
+
+ /**
+ * Performs a mouse wheel scroll at the current virtual mouse position. RemoteInput natively adds
+ * a small float value to this, to simulate human imperfection
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @param x The x co-ordinate of the input
+ * @param y The y co-ordinate of the input
+ * @param lines The number of notches to scroll; positive for down, negative for up
+ */
+ void EIOS_ScrollMouse(Pointer eios, int x, int y, int lines);
+
+ /**
+ * Types out a string of characters whilst compensating for the need of modifier keys. Useful when
+ * typing something to a dialogue box, will compensate for special characters, however lacks delay
+ * between keypresses.
+ *
+ * @param eios The pointer to the paired EIOS target instance
+ * @param string The text to be typed
+ * @param keywait The time in milliseconds to hold down a key
+ * @param keymodwait The time in milliseconds to hold down modifier keys
+ */
+ void EIOS_SendString(Pointer eios, String string, int keywait, int keymodwait);
+}
diff --git a/src/main/java/com/chromascape/utils/core/runtime/exception/ScriptStoppedException.java b/src/main/java/com/chromascape/utils/core/runtime/exception/ScriptStoppedException.java
index a509460..80e9697 100644
--- a/src/main/java/com/chromascape/utils/core/runtime/exception/ScriptStoppedException.java
+++ b/src/main/java/com/chromascape/utils/core/runtime/exception/ScriptStoppedException.java
@@ -1,19 +1,19 @@
-package com.chromascape.utils.core.runtime.exception;
-
-/**
- * Exception used to indicate that a running script has been requested to stop.
- *
- * This unchecked exception is thrown internally to signal that the script execution should be
- * terminated gracefully. It can be caught by the script runner to halt execution without treating
- * the stop as an error.
- *
- * Typically, this exception is thrown by calling {@code stop()} methods in the script lifecycle
- * to immediately exit the current execution cycle.
- */
-public class ScriptStoppedException extends RuntimeException {
-
- /** Constructs a new ScriptStoppedException with a default message. */
- public ScriptStoppedException() {
- super("Script stopped");
- }
-}
+package com.chromascape.utils.core.runtime.exception;
+
+/**
+ * Exception used to indicate that a running script has been requested to stop.
+ *
+ * This unchecked exception is thrown internally to signal that the script execution should be
+ * terminated gracefully. It can be caught by the script runner to halt execution without treating
+ * the stop as an error.
+ *
+ * Typically, this exception is thrown by calling {@code stop()} methods in the script lifecycle
+ * to immediately exit the current execution cycle.
+ */
+public class ScriptStoppedException extends RuntimeException {
+
+ /** Constructs a new ScriptStoppedException with a default message. */
+ public ScriptStoppedException() {
+ super("Script stopped");
+ }
+}
diff --git a/src/main/java/com/chromascape/utils/core/screen/topology/ColourContours.java b/src/main/java/com/chromascape/utils/core/screen/topology/ColourContours.java
index 1f2281d..2c679ff 100644
--- a/src/main/java/com/chromascape/utils/core/screen/topology/ColourContours.java
+++ b/src/main/java/com/chromascape/utils/core/screen/topology/ColourContours.java
@@ -179,9 +179,7 @@ public static List Provides methods for:
- *
- * Unlike the standard {@link Robot} class which captures the composite screen (including
- * overlays), this method directly reads the Device Context (DC) of the target window. This allows
- * for capturing the game view cleanly even when external overlays are drawn on top of it.
+ * Captures a {@link Rectangle} region on the client screen, intended to be used when
+ * screenshotting zones for template matching and or colour extraction.
*
- * This method performs a manual memory copy from native GDI resources to a Java {@link
- * BufferedImage} to ensure compatibility and performance.
- *
- * @return A {@link BufferedImage} containing the window contents in BGR format, suitable for
- * OpenCV processing.
+ * @param zone The rectangle area in client relative screen co-ordinates
+ * @return A {@link BufferedImage} of the captured area
*/
- public static BufferedImage captureWindow() {
- RECT bounds = new RECT();
- User32.INSTANCE.GetClientRect(canvasHwnd, bounds);
- int width = bounds.right - bounds.left;
- int height = bounds.bottom - bounds.top;
-
- return performCapture(0, 0, width, height);
+ public static BufferedImage captureZone(Rectangle zone) {
+ BufferedImage screen = captureWindow();
+ if (screen == null) {
+ throw new RuntimeException("Screen could not be captured");
+ }
+ return screen.getSubimage(zone.x, zone.y, zone.width, zone.height);
}
/**
- * Captures a specific rectangular region of the screen directly from the window's Device Context.
- *
- * This is critical for high frequency checks like OCR or HP bars, where we only need a tiny
- * screen region.
+ * Grabs the latest rendered frame of the target application, regardless of if the client is
+ * maximised, minimised, partially or fully covered. This is to be used with template matching and
+ * {@link com.chromascape.utils.core.screen.topology.ChromaObj} detection.
*
- * @param zone The screen-space {@link Rectangle} to capture.
- * @return A {@link BufferedImage} containing only the requested region.
+ * @return A {@link BufferedImage} of the client's screen
*/
- public static BufferedImage captureZone(Rectangle zone) {
- Rectangle clientRect = toClientBounds(new Rectangle(zone));
-
- return performCapture(clientRect.x, clientRect.y, clientRect.width, clientRect.height);
- }
+ public static synchronized BufferedImage captureWindow() {
+ Rectangle dims = remoteInput.getTargetDimensions();
+ int width = dims.width;
+ int height = dims.height;
- /** Internal helper to perform GDI BitBlt and pixel conversion. */
- private static BufferedImage performCapture(int x, int y, int w, int h) {
- if (w <= 0 || h <= 0) {
- return new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR);
+ if (width <= 0 || height <= 0) {
+ return null;
}
- HDC hdcSrc = User32.INSTANCE.GetDC(canvasHwnd);
- HDC hdcMem = GDI32.INSTANCE.CreateCompatibleDC(hdcSrc);
- HBITMAP hBitmap = GDI32.INSTANCE.CreateCompatibleBitmap(hdcSrc, w, h);
- HANDLE hOld = GDI32.INSTANCE.SelectObject(hdcMem, hBitmap);
-
- boolean success = GDI32.INSTANCE.BitBlt(hdcMem, 0, 0, w, h, hdcSrc, x, y, 0x00CC0020);
-
- BufferedImage bgr = null;
-
- if (success) {
- BITMAPINFO bmi = new BITMAPINFO();
- bmi.bmiHeader.biWidth = w;
- bmi.bmiHeader.biHeight = -h;
- bmi.bmiHeader.biPlanes = 1;
- bmi.bmiHeader.biBitCount = 32;
- bmi.bmiHeader.biCompression = WinGDI.BI_RGB;
-
- Memory buffer = new Memory((long) w * h * 4);
- GDI32.INSTANCE.GetDIBits(hdcMem, hBitmap, 0, h, buffer, bmi, WinGDI.DIB_RGB_COLORS);
-
- BufferedImage argb = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
- int[] pixels = buffer.getIntArray(0, w * h);
- int[] targetPixels = ((DataBufferInt) argb.getRaster().getDataBuffer()).getData();
- System.arraycopy(pixels, 0, targetPixels, 0, pixels.length);
-
- bgr = new BufferedImage(w, h, BufferedImage.TYPE_3BYTE_BGR);
- Graphics g = bgr.getGraphics();
- g.drawImage(argb, 0, 0, null);
- g.dispose();
+ if (screenBuffer == null) {
+ screenBuffer = remoteInput.getImageBuffer();
}
- GDI32.INSTANCE.SelectObject(hdcMem, hOld);
- GDI32.INSTANCE.DeleteObject(hBitmap);
- GDI32.INSTANCE.DeleteDC(hdcMem);
- User32.INSTANCE.ReleaseDC(canvasHwnd, hdcSrc);
+ int bufferSize = width * height * 4;
+ byte[] data = screenBuffer.getByteArray(0, bufferSize);
- if (bgr == null) {
- throw new RuntimeException("GDI Capture failed");
- }
- return bgr;
+ return createBufferedImage(data, width, height);
}
/**
- * Gets the bounds of the (game view) RuneLite AWT Canvas object.
+ * Internal helper to create a buffered image from a C++ style byte array of pixels in BGRA
+ * format.
*
- * Converts the client-relative origin to screen coordinates using {@code ClientToScreen}.
- *
- * @return A {@link Rectangle} representing the on-screen position and size of RuneLite's client
- * area excluding possible window borders, title or scrollbars.
+ * @param pixels The byte array of pixel data in BGRA format
+ * @param width The width of the client in pixels
+ * @param height The height of the client in pixels
+ * @return A {@link BufferedImage} representing the image
*/
- public static Rectangle getWindowBounds() {
- WinDef.RECT dimensions = new WinDef.RECT();
- User32.INSTANCE.GetClientRect(canvasHwnd, dimensions);
-
- WinDef.POINT clientTopLeft = new WinDef.POINT();
- clientTopLeft.x = 0;
- clientTopLeft.y = 0;
-
- User32Extended uex = User32Extended.INSTANCE;
- uex.ClientToScreen(canvasHwnd, clientTopLeft);
-
- return new Rectangle(
- clientTopLeft.x,
- clientTopLeft.y,
- dimensions.right - dimensions.left,
- dimensions.bottom - dimensions.top);
+ private static BufferedImage createBufferedImage(byte[] pixels, int width, int height) {
+ DataBufferByte buffer = new DataBufferByte(pixels, pixels.length);
+ WritableRaster raster =
+ Raster.createInterleavedRaster(
+ buffer, width, height, width * 4, 4, new int[] {2, 1, 0, 3}, null);
+
+ ColorModel cm =
+ new ComponentColorModel(
+ ColorSpace.getInstance(ColorSpace.CS_sRGB),
+ new int[] {8, 8, 8, 8},
+ true,
+ false,
+ Transparency.TRANSLUCENT,
+ DataBuffer.TYPE_BYTE);
+
+ return new BufferedImage(cm, raster, false, null);
}
/**
- * Converts a screen-space {@link Rectangle} to RuneLite game-view canvas local coordinates.
- *
- * This method adjusts the rectangle's position by subtracting the top-left corner of the
- * canvas (as determined by {@link ScreenManager#getWindowBounds()}) from its {@code x} and {@code
- * y} coordinates. This is necessary when working with screen-detected regions (e.g., from
- * template matching) and applying them to canvas-local images.
- *
- * Note: This method mutates and returns the original {@code Rectangle} instance.
+ * Gets the bounds of the (game view) RuneLite AWT Canvas object.
*
- * @param screenBounds the rectangle in absolute screen coordinates
- * @return the same rectangle, now adjusted to canvas-local coordinates
+ * @return A {@link Rectangle} representing the size of RuneLite's client area, excluding possible
+ * window borders, title or scrollbars.
*/
- public static Rectangle toClientBounds(Rectangle screenBounds) {
- Rectangle offset = ScreenManager.getWindowBounds();
- screenBounds.x -= offset.x;
- screenBounds.y -= offset.y;
- return screenBounds;
+ public static Rectangle getWindowBounds() {
+ return remoteInput.getTargetDimensions();
}
/**
- * Converts a screen-space {@link Point} to RuneLite game-view canvas local coordinates.
- *
- * This method adjusts the point's position by subtracting the top-left corner of the canvas
- * (as returned by {@link ScreenManager#getWindowBounds()}). This is typically used when
- * translating points detected in full-screen captures into the coordinate space of the
- * canvas-local window image.
+ * Sets the RemoteInput object in the ScreenManager, allowing it to access to the client's screen
+ * buffer.
*
- * @param screenPoint the point in absolute screen coordinates
- * @return a new {@code Point} adjusted to canvas-local coordinates
+ * @param remoteInput The {@link RemoteInput} object
*/
- public static Point toClientCoords(Point screenPoint) {
- Rectangle offset = ScreenManager.getWindowBounds();
- return new Point(screenPoint.x - offset.x, screenPoint.y - offset.y);
+ public static void setRemoteInput(RemoteInput remoteInput) {
+ ScreenManager.remoteInput = remoteInput;
}
}
diff --git a/src/main/java/com/chromascape/utils/core/screen/window/WindowHandler.java b/src/main/java/com/chromascape/utils/core/screen/window/WindowHandler.java
deleted file mode 100644
index 8c4f3f8..0000000
--- a/src/main/java/com/chromascape/utils/core/screen/window/WindowHandler.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package com.chromascape.utils.core.screen.window;
-
-import com.sun.jna.Native;
-import com.sun.jna.Pointer;
-import com.sun.jna.platform.win32.WinDef.HWND;
-import com.sun.jna.platform.win32.WinUser.WNDENUMPROC;
-import com.sun.jna.ptr.IntByReference;
-import com.sun.jna.win32.StdCallLibrary;
-import java.util.concurrent.atomic.AtomicReference;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Utility class for locating and identifying a specific native window (e.g., "RuneLite") on the
- * Windows operating system using JNA and Win32 APIs.
- */
-public class WindowHandler {
- private static final Logger logger = LoggerFactory.getLogger(WindowHandler.class);
- private static final String WINDOW_NAME = "RuneLite";
-
- /**
- * JNA interface for accessing low-level Win32 User32 functions that are not provided by the
- * default JNA platform mappings.
- */
- public interface User32 extends StdCallLibrary {
- User32 INSTANCE = Native.load("user32", User32.class);
-
- /**
- * Enumerates all top-level windows on the screen by invoking the provided callback.
- *
- * @param lpEnumFunc The callback to be called for each window.
- * @param arg A user-defined value passed to the callback (usually null).
- */
- void EnumWindows(WNDENUMPROC lpEnumFunc, Pointer arg);
-
- /**
- * Retrieves the title text of the specified window.
- *
- * @param hwnd Handle to the window.
- * @param lpString Buffer that receives the window title.
- * @param maxCount Maximum number of characters to copy.
- */
- void GetWindowTextA(HWND hwnd, byte[] lpString, int maxCount);
-
- /**
- * Retrieves the process identifier (PID) for the specified window.
- *
- * @param hwnd Handle to the window.
- * @param lpDword Receives the process ID.
- */
- void GetWindowThreadProcessId(HWND hwnd, IntByReference lpDword);
-
- /**
- * Enumerates all child windows of a specified parent window.
- *
- * This function calls the provided callback {@code lpEnumFunc} for each child window of the
- * specified parent {@code hWndParent}. It is commonly used to locate specific child controls or
- * canvases within a larger window.
- *
- * @param hwndParent the handle of the parent window whose children are to be enumerated
- * @param lpEnumFunc the callback function to be called for each child window
- * @param data a pointer to user-defined data that will be passed to the callback
- */
- void EnumChildWindows(HWND hwndParent, WNDENUMPROC lpEnumFunc, Pointer data);
-
- /**
- * Retrieves the class name of a specified window.
- *
- * This function writes the window class name of {@code hwnd} into the provided byte array
- * {@code lpString}, up to a maximum of {@code maxCount} characters. It is useful for
- * identifying specific child windows or controls by class.
- *
- * @param hwnd the handle of the window whose class name is to be retrieved
- * @param lpString the byte array to receive the class name
- * @param maxCount the maximum number of characters to copy into {@code lpString}
- */
- void GetClassNameA(HWND hwnd, byte[] lpString, int maxCount);
- }
-
- /**
- * Attempts to locate the window whose title matches the {@code WINDOW_NAME}.
- *
- * @return The {@link HWND} handle of the target window, or {@code null} if not found.
- */
- public static HWND getTargetWindow() {
- AtomicReference This is useful when multiple child windows share the same class (e.g., RuneLite has two
- * SunAwtCanvas children: the outer canvas with black bars and the inner canvas with the game
- * viewport). By specifying an index, you can reliably retrieve the intended child.
- *
- * @param parent the {@link HWND} of the parent window whose children are to be enumerated
- * @param className the class name of the child windows to match (e.g., "SunAwtCanvas")
- * @param n the 1-based index of the matching child window to retrieve (1 = first match)
- * @return the {@link HWND} of the N-th matching child window, or {@code null} if no such child
- * exists
- */
- public static HWND findNthChildWindow(HWND parent, String className, int n) {
- AtomicReference
+ *
+ * "The WindMouse algorithm is inspired by highschool physics that me-of-fifteen-years-ago was just
+ * getting interested in. The cursor is modeled as an object with some inertia (mass) that is acted
+ * on by two forces:
*
*
+ *
+ *
- *
*
- *
- *
- *
- *
- *
+ * Utility class for capturing screen regions and retrieving window bounds. Screen capture utilities
+ * are intended to be used with colour contour extraction and template matching.
*/
public class ScreenManager {
- /**
- * Grabs the HWND of the second child of the RuneLite window - The game view portion. This is
- * prone to breaking if RuneLite add more canvas elements, but this is not likely.
- */
- private static final HWND canvasHwnd =
- WindowHandler.findNthChildWindow(WindowHandler.getTargetWindow(), "SunAwtCanvas", 2);
-
- /**
- * JNA extension interface to allow calling {@code ClientToScreen} which converts window-relative
- * coordinates to screen coordinates.
- */
- public interface User32Extended extends User32 {
+ private static RemoteInput remoteInput;
- /**
- * Converts the client-relative point to screen coordinates.
- *
- * @param hwnd the window handle of the client.
- * @param point the point to convert.
- */
- void ClientToScreen(HWND hwnd, POINT point);
-
- /**
- * Retrieves the coordinates of a window's client area. The client coordinates specify the
- * upper-left and lower-right corners of the client area. Because client coordinates are
- * relative to the upper-left corner of a window's client area, the coordinates of the
- * upper-left corner are (0,0). Link
- * to documentation
- *
- * @param hwnd Handle to the window.
- * @param rect Long pointer to a RECT structure that receives the client coordinates. The left
- * and top members are zero. The right and bottom members contain the width and height of
- * the window.
- * @return True if succeeded, false otherwise.
- */
- boolean GetClientRect(HWND hwnd, RECT rect);
-
- User32Extended INSTANCE = Native.load("user32", User32Extended.class);
- }
+ private static Pointer screenBuffer = null;
/**
- * Captures the entire content of the game window using the Windows GDI BitBlt function.
- *
- *
- ${author}"
+ notes="${notes//\\n/
}"
+ notes="${notes//$'\n'/
}"
+ elif [ "${{ github.event_name }}" = "pull_request" ]; then
+ commit="${{ github.event.pull_request.title }}"
+ body="${{ github.event.pull_request.body }}"
+ author="${{ github.event.pull_request.author }}"
+ notes="${commit}
${body}
- ${author}"
+ notes="${notes//\\n/
}"
+ notes="${notes//$'\n'/
}"
+ fi
+ echo ::set-output name=description::"${notes}"
+ shell: bash
+
+ - name: Download Artifacts
+ if: github.event_name == 'push'
+ uses: actions/download-artifact@v4
+ with:
+ pattern: 'artifact-*'
+ path: 'artifact'
+ merge-multiple: true
+
+ - name: List Artifacts
+ run: |
+ ls artifact
+
+ - name: Upload Autobuild
+ if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v')
+ uses: Brandon-T/update-release-action@v1
+ with:
+ github_token: ${{ secrets.CI_RELEASE_TOKEN }}
+ release_name: 'Auto-Build'
+ file: 'artifact/*'
+ is_file_glob: true
+ overwrite: true
+ release_notes: ${{ steps.release_notes.outputs.description }}
+ deletes_existing_release: true
+ pre_release: false
+ prefix_branch_name: false
+ suffix_branch_name: false
+ draft_release: false
+ retry_count: 2
+ retry_delay: 5
+ owner: '${{ github.repository_owner }}'
+ repo: '${{ matrix.config.target_release_repo }}'
+ tag: 'autobuild'
+ bump_tag: true
+ ref: 'master'
+
+ - name: Upload Release Build
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
+ uses: Brandon-T/update-release-action@v1
+ with:
+ github_token: ${{ secrets.CI_RELEASE_TOKEN }}
+ release_name: ${{ steps.release_notes.outputs.version }}
+ file: 'artifact/*'
+ is_file_glob: true
+ overwrite: true
+ release_notes: ${{ steps.release_notes.outputs.description }}
+ deletes_existing_release: false
+ pre_release: false
+ prefix_branch_name: false
+ suffix_branch_name: false
+ draft_release: false
+ retry_count: 2
+ retry_delay: 5
+ owner: '${{ github.repository_owner }}'
+ repo: '${{ matrix.config.target_release_repo }}'
+ tag: ${{ steps.release_notes.outputs.version }}
+ bump_tag: true
+ ref: 'master'
diff --git a/third-party/RemoteInput/.github/workflows/build.zip b/third-party/RemoteInput/.github/workflows/build.zip
new file mode 100644
index 0000000..35210e3
Binary files /dev/null and b/third-party/RemoteInput/.github/workflows/build.zip differ
diff --git a/third-party/RemoteInput/.gitignore b/third-party/RemoteInput/.gitignore
new file mode 100644
index 0000000..86330f2
--- /dev/null
+++ b/third-party/RemoteInput/.gitignore
@@ -0,0 +1,37 @@
+# Prerequisites
+*.d
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+*.smod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+/cmake-build-*
+
+# Precompiled folder and binaries
+!/precompiled
+!/precompiled/**
\ No newline at end of file
diff --git a/third-party/RemoteInput/CMakeLists.txt b/third-party/RemoteInput/CMakeLists.txt
new file mode 100644
index 0000000..8158110
--- /dev/null
+++ b/third-party/RemoteInput/CMakeLists.txt
@@ -0,0 +1,418 @@
+cmake_minimum_required(VERSION 3.15)
+project(RemoteInput VERSION 1.0.0 DESCRIPTION "Remote Input")
+
+set(CMAKE_CXX_STANDARD 20)
+
+# ----------------------------- BUILD -----------------------------
+IF(NOT CMAKE_BUILD_TYPE)
+ SET(CMAKE_BUILD_TYPE Release)
+ENDIF()
+
+set(PYTHON_BINDINGS ON)
+set(USE_PYBIND11 OFF)
+set(USE_PYTHON3 OFF)
+set(USE_SYSTEM_PYBIND11 OFF)
+set(PYTHON_LIMITED_VERSION 0x03080000)
+
+IF (USE_PYBIND11)
+ unset(Py_LIMITED_API)
+ MESSAGE(STATUS, "PyBind11 being used -- Ignoring Py_LIMITED_API")
+ENDIF()
+
+IF (NOT USE_PYBIND11)
+ set(Py_LIMITED_API ${PYTHON_LIMITED_VERSION})
+ENDIF()
+
+IF(PYTHON_BINDINGS AND USE_PYBIND11 AND Py_LIMITED_API)
+ MESSAGE(FATAL_ERROR, "PyBind11 cannot be used with Py_LIMITED_API")
+ENDIF()
+
+# ----------------------------- PACKAGES -----------------------------
+set(JAVA_AWT_LIBRARY NotNeeded)
+set(JAVA_JVM_LIBRARY NotNeeded)
+set(JAVA_AWT_INCLUDE_PATH NotNeeded)
+
+find_package(Java 1.8 REQUIRED)
+find_package(JNI 1.8 REQUIRED)
+
+IF(PYTHON_BINDINGS)
+ IF(USE_PYBIND11)
+ IF(USE_PYTHON3)
+ find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
+ set(PY_INCLUDE_DIRS ${Python3_INCLUDE_DIRS})
+ set(PY_LIBRARIES ${Python3_LIBRARIES})
+ set(PY_LIBRARIES ${Python3_LIBRARY_DIRS})
+ set(PY_LINK_OPTIONS ${Python3_LINK_OPTIONS})
+ set(PY_DYNAMIC_LINKER_FLAGS ${Python3_DYNAMIC_LINKER_FLAGS})
+ ELSE()
+ find_package(Python REQUIRED COMPONENTS Interpreter Development)
+ set(PY_INCLUDE_DIRS ${Python_INCLUDE_DIRS})
+ set(PY_LIBRARIES ${Python_LIBRARIES})
+ set(PY_LIBRARY_DIRS ${Python_LIBRARY_DIRS})
+ set(PY_LINK_OPTIONS ${Python_LINK_OPTIONS})
+ set(PY_DYNAMIC_LINKER_FLAGS ${Python_DYNAMIC_LINKER_FLAGS})
+ ENDIF()
+
+ IF(USE_SYSTEM_PYBIND11)
+ find_package(nanobind REQUIRED)
+ ENDIF()
+ ELSE()
+ IF(USE_PYTHON3)
+ find_package(Python3 3.8 REQUIRED COMPONENTS Interpreter Development)
+ set(PY_INCLUDE_DIRS ${Python3_INCLUDE_DIRS})
+ # set(PY_LIBRARIES ${Python3_LIBRARIES})
+ set(PY_LIBRARY_DIRS ${Python3_LIBRARY_DIRS})
+ set(PY_LINK_OPTIONS ${Python3_LINK_OPTIONS})
+ set(PY_DYNAMIC_LINKER_FLAGS ${Python3_DYNAMIC_LINKER_FLAGS})
+ ELSE()
+ find_package(Python 3.8 REQUIRED COMPONENTS Interpreter Development)
+ set(PY_INCLUDE_DIRS ${Python_INCLUDE_DIRS})
+ # set(PY_LIBRARIES ${Python_LIBRARIES})
+ set(PY_LIBRARY_DIRS ${Python_LIBRARY_DIRS})
+ set(PY_LINK_OPTIONS ${Python_LINK_OPTIONS})
+ set(PY_DYNAMIC_LINKER_FLAGS ${Python_DYNAMIC_LINKER_FLAGS})
+ ENDIF()
+ ENDIF()
+ MESSAGE(STATUS, "${PY_INCLUDE_DIRS}")
+ MESSAGE(STATUS, "${PY_LIBRARIES}")
+ MESSAGE(STATUS, "${PY_LIBRARY_DIRS}")
+ MESSAGE(STATUS, "${PY_DYNAMIC_LINKER_FLAGS}")
+ENDIF()
+
+# ----------------------- INCLUDE_DIRECTORIES -----------------------
+set(INCLUDE_DIRECTORIES
+ RemoteInput
+ RemoteInput/Echo
+ RemoteInput/Hooks
+ RemoteInput/Java
+ RemoteInput/Platform
+ RemoteInput/Plugin
+ RemoteInput/Plugin/JVM
+ ${JNI_INCLUDE_DIRS}
+ ${PY_INCLUDE_DIRS})
+
+
+# ----------------------------- PLATFORM -----------------------------
+IF(WIN32)
+ set(EXTRA_INCLUDES
+ ${OPENGL_INCLUDES})
+ELSEIF(APPLE)
+ find_path(FOUNDATION_INCLUDES Foundation/Foundation.h)
+ find_path(COCOA_INCLUDES Cocoa/Cocoa.h)
+ find_path(OPENGL_INCLUDES OpenGL/OpenGL.h)
+
+ set(EXTRA_INCLUDES
+ ${FOUNDATION_INCLUDES}
+ ${COCOA_INCLUDES}
+ ${OPENGL_INCLUDES})
+ELSE()
+ set(EXTRA_INCLUDES
+ ${OPENGL_INCLUDES})
+ENDIF()
+
+
+
+# ----------------------------- LINKER -----------------------------
+IF(WIN32)
+ set(EXTRA_LIBRARIES
+ user32
+ opengl32
+ gdi32
+ dbghelp) # Needed for ThirdParty/kubo/injector
+ELSEIF(APPLE)
+ find_library(FOUNDATION Foundation)
+ find_library(COCOA Cocoa)
+ find_library(OPENGL OpenGL)
+
+ set(EXTRA_LIBRARIES
+ ${FOUNDATION}
+ ${COCOA}
+ ${OPENGL}
+ dl
+ pthread)
+ELSE()
+ set(EXTRA_LIBRARIES
+ dl
+ pthread
+ GL
+ rt
+ X11)
+ENDIF()
+
+
+# ----------------------------- SOURCES -----------------------------
+
+set(SRC_LIST
+ ${EXTRA_INCLUDES}
+ RemoteInput/RemoteInput.h
+ RemoteInput/Echo/Atomics.cxx
+ RemoteInput/Echo/Atomics.hxx
+ RemoteInput/Echo/Mutex.cxx
+ RemoteInput/Echo/Mutex.hxx
+ RemoteInput/Echo/Semaphore.cxx
+ RemoteInput/Echo/Semaphore.hxx
+ RemoteInput/Echo/Event.cxx
+ RemoteInput/Echo/Event.hxx
+ RemoteInput/Echo/Synchronization.hxx
+ RemoteInput/Echo/MemoryMap.cxx
+ RemoteInput/Echo/MemoryMap.hxx
+ RemoteInput/Echo/MemoryMapStream.hxx
+ RemoteInput/Echo/Module.cxx
+ RemoteInput/Echo/Module.hxx
+ RemoteInput/Echo/Stream.cxx
+ RemoteInput/Echo/Stream.hxx
+ RemoteInput/Echo/Time.cxx
+ RemoteInput/Echo/Time.hxx
+ RemoteInput/Echo/TypeTraits.hxx
+ RemoteInput/Echo/TypeTraits_Functional.hxx
+ RemoteInput/Echo/TypeTraits_Functional_Attributes.hxx
+ RemoteInput/Java/JNI_Common.hxx
+ RemoteInput/Java/Applet.cxx
+ RemoteInput/Java/Applet.hxx
+ RemoteInput/Java/AWTAccessor.cxx
+ RemoteInput/Java/AWTAccessor.hxx
+ RemoteInput/Java/AWTEvent.cxx
+ RemoteInput/Java/AWTEvent.hxx
+ RemoteInput/Java/AWTEventAccessor.cxx
+ RemoteInput/Java/AWTEventAccessor.hxx
+ RemoteInput/Java/Component.cxx
+ RemoteInput/Java/Component.hxx
+ RemoteInput/Java/Container.cxx
+ RemoteInput/Java/Container.hxx
+ RemoteInput/Java/EventQueue.cxx
+ RemoteInput/Java/EventQueue.hxx
+ RemoteInput/Java/FocusEvent.cxx
+ RemoteInput/Java/FocusEvent.hxx
+ RemoteInput/Java/Frame.cxx
+ RemoteInput/Java/Frame.hxx
+ RemoteInput/Java/InputEvent.cxx
+ RemoteInput/Java/InputEvent.hxx
+ RemoteInput/Java/KeyEvent.cxx
+ RemoteInput/Java/KeyEvent.hxx
+ RemoteInput/Java/MouseEvent.cxx
+ RemoteInput/Java/MouseEvent.hxx
+ RemoteInput/Java/MouseWheelEvent.cxx
+ RemoteInput/Java/MouseWheelEvent.hxx
+ RemoteInput/Java/PointerInfo.cxx
+ RemoteInput/Java/PointerInfo.hxx
+ RemoteInput/Java/SunToolkit.cxx
+ RemoteInput/Java/SunToolkit.hxx
+ RemoteInput/Java/Toolkit.cxx
+ RemoteInput/Java/Toolkit.hxx
+ RemoteInput/Java/Window.cxx
+ RemoteInput/Java/Window.hxx
+ RemoteInput/Java/WindowEvent.cxx
+ RemoteInput/Java/WindowEvent.hxx
+ RemoteInput/Java/RIEventQueue.cxx
+ RemoteInput/Java/RIEventQueue.hxx
+ RemoteInput/Platform/DebugConsole.cxx
+ RemoteInput/Platform/DebugConsole.hxx
+ RemoteInput/Platform/JavaInternal.hxx
+ RemoteInput/Platform/NativeHooks.hxx
+ RemoteInput/Platform/NativeHooks_Darwin.cxx
+ RemoteInput/Platform/NativeHooks_Linux.cxx
+ RemoteInput/Platform/NativeHooks_Windows.cxx
+ RemoteInput/Platform/Platform.hxx
+ RemoteInput/Platform/Platform_Linux.cxx
+ RemoteInput/Platform/Platform_Windows.cxx
+ RemoteInput/Plugin/ControlCenter.cxx
+ RemoteInput/Plugin/ControlCenter.hxx
+ RemoteInput/Plugin/Graphics.cxx
+ RemoteInput/Plugin/Graphics.hxx
+ RemoteInput/Plugin/ImageData.cxx
+ RemoteInput/Plugin/ImageData.hxx
+ RemoteInput/Plugin/InputOutput.cxx
+ RemoteInput/Plugin/InputOutput.hxx
+ RemoteInput/Plugin/NativePlugin.cxx
+ RemoteInput/Plugin/NativePlugin.hxx
+ RemoteInput/Plugin/Plugin.cxx
+ RemoteInput/Plugin/Plugin.hxx
+ RemoteInput/Plugin/Signal.hxx
+ RemoteInput/Plugin/SimbaPlugin.cxx
+ RemoteInput/Plugin/SimbaPlugin.hxx
+ RemoteInput/Plugin/TMemoryManager.hxx
+ RemoteInput/Plugin/JVM/JVMCache.cxx
+ RemoteInput/Plugin/JVM/JVMCache.hxx
+ RemoteInput/Plugin/JVM/RemoteVM.hxx
+ RemoteInput/Plugin/JVM/RemoteVM.cxx
+ RemoteInput/DetachedThreadPool.cxx
+ RemoteInput/DetachedThreadPool.hxx
+ RemoteInput/EIOS.cxx
+ RemoteInput/EIOS.hxx
+ RemoteInput/JVM.cxx
+ RemoteInput/JVM.hxx
+ RemoteInput/Random.cxx
+ RemoteInput/Random.hxx
+ RemoteInput/Reflection.cxx
+ RemoteInput/Reflection.hxx
+ RemoteInput/ReflectionHook.cxx
+ RemoteInput/ReflectionHook.hxx
+ RemoteInput/RemoteInput.h
+ RemoteInput/ThreadPool.cxx
+ RemoteInput/ThreadPool.hxx
+ RemoteInput/Injection/Injector.hxx
+ RemoteInput/Injection/Injector_Windows.cxx
+ RemoteInput/Injection/Injector_Darwin.cxx
+ RemoteInput/Injection/Injector_Linux.cpp
+ RemoteInput/Injection/Injector_Arm.cpp)
+
+IF(APPLE)
+ list(APPEND SRC_LIST
+ RemoteInput/Platform/Platform_Darwin.mm)
+ENDIF()
+
+IF(PYTHON_BINDINGS)
+ list(APPEND SRC_LIST
+ RemoteInput/Plugin/Python/PythonMacros.hxx
+ RemoteInput/Plugin/Python/PythonCommon.cxx
+ RemoteInput/Plugin/Python/PythonCommon.hxx
+ RemoteInput/Plugin/Python/PythonPlugin.cxx
+ RemoteInput/Plugin/Python/PythonPlugin.hxx
+ RemoteInput/Plugin/Python/PythonEIOS.cxx
+ RemoteInput/Plugin/Python/PythonEIOS.hxx
+ RemoteInput/Plugin/Python/PythonJavaObject.cxx
+ RemoteInput/Plugin/Python/PythonJavaObject.hxx
+ RemoteInput/Plugin/Python/PythonJavaArray.cxx
+ RemoteInput/Plugin/Python/PythonJavaArray.hxx
+ RemoteInput/Plugin/Python/PythonJavaList.cxx
+ RemoteInput/Plugin/Python/PythonJavaList.hxx
+ RemoteInput/Plugin/Python/PythonCommon_Templates.hxx
+ RemoteInput/Plugin/Python/Python.cxx
+ RemoteInput/Plugin/Python/Python.hxx)
+ENDIF()
+
+IF(PYTHON_BINDINGS AND NOT (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC"))
+ # set(EXTRA_LIBRARIES
+ # ${EXTRA_LIBRARIES}
+ # ${Python_LIBRARIES})
+ # set(PYTHON_DYNAMIC_LINKER_FLAGS
+ # -undefined dynamic_lookup)
+ENDIF()
+
+# ---------------------------- COMPILE ----------------------------
+
+IF(PYTHON_BINDINGS AND USE_PYBIND11)
+ add_subdirectory(RemoteInput/Thirdparty)
+
+ IF(NOT USE_SYSTEM_PYBIND11)
+ add_subdirectory(RemoteInput/Thirdparty/nanobind)
+ ENDIF()
+
+ nanobind_add_module(${PROJECT_NAME} SHARED ${SRC_LIST} $
+