Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
13 changes: 4 additions & 9 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@
# Virtual machine crash logs
hs_err_pid*

# Generated files
bin/
!third_party/KInput/KInput/KInput/bin/
!third_party/KInput/KInput/KInputCtrl/bin/
out/
build/
!third_party/minhook/build/

# Gradle cache and wrappers
.gradle/
!gradle/wrapper/gradle-wrapper.jar
Expand Down Expand Up @@ -67,4 +59,7 @@ secrets.properties

# custom user scripts
/src/main/java/com/chromascape/scripts/*/
/scripts/*/
/scripts/*/

# Ignore build directory, even though it was ignored before??
/build/
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,13 @@ https://github.com/user-attachments/assets/1267df86-db5c-4189-8c8d-e2f5fe047cab

ChromaScape uses advanced remote input techniques to function as a virtual second mouse dedicated to the client window. Unlike traditional input methods, this approach never hijacks your physical mouse, so you can continue using your PC without interruption while the bot runs in the background.

This is achieved through KInput.
RemoteInput also allows the user to completely minimise RuneLite while the bot is running

ChromaScape uses a slightly modified version of the [64-bit](https://github.com/ThatOneGuyScripts/KInput) supported version of KInput. The [original](https://github.com/Kasi-R/KInput) KInput source is also available for reference.
There are instructions on how to build KInput from source within the third_party directory in `DEV_README.md`
This is achieved through Brandon-T's RemoteInput.

The [original](https://github.com/Brandon-T/RemoteInput) RemoteInput source is also available for reference.
There are instructions on how to build RemoteInput from source within the third-party directory in `DEV_README.md`.
For those who prefer, pre-built binaries are available, so the project will work out of the box.

### - Humanised mouse movement
To further reduce bot detection risks, ChromaScape uses an adapted version of [WindMouse](https://ben.land/post/2021/04/25/windmouse-human-mouse-movement/), a physics based calculation of gravity and wind to ensure pixel imperfections unlike bezier curves. WindMouse has been a successful staple within the community for over a decade, and provides exceedingly human mouse movements.
Expand All @@ -73,9 +76,6 @@ https://github.com/user-attachments/assets/b1c601e9-b58a-4a54-865b-739a25ddf898

The UI is built with Spring Boot, a mature industry framework, and served locally. This gives you a powerful way to view logs, manage scripts, see the bot's sensor information, and extend functionality. It's fully customizable with basic HTML/CSS/JS, so power users can tweak or overhaul it without modifying core framework utilities or needing to worry about tight coupling.

**Newly: you can now have runelite covered by other applications while the bot runs**
> You can't minimise it or put it entirely offscreen yet, but you can place other applications on top and have it run in the background.

## Colour and Image Detection

### - Colour Picker
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
}

group = "com.chromascape"
version = "0.3.0-SNAPSHOT"
version = "0.3.0"

// Customize build directories - put DLLs in build/dist
layout.buildDirectory.set(file("build"))
Expand Down
5 changes: 2 additions & 3 deletions config/checkstyle/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
<suppressions>
<suppress files=".*Kinput\.java" checks="AbbreviationAsWordInName"/>
<suppress files=".*Kinput\.java" checks="MethodName"/>
<suppress files=".*WindowHandler\.java" checks="MethodName"/>
<suppress files=".*ScreenManager\.java" checks="MethodName"/>
<suppress files=".*WindowsProcessManager\.java" checks="MethodName"/>
<suppress files=".*Similarity\.java" checks=".*"/>
<suppress files=".*WindMouse\.java" checks=".*"/>
<suppress files=".*RemoteInputInterface\.java" checks=".*"/>
<suppress files=".*TemplateMatching\.java" checks="AvoidStarImport"/>
<suppress files=".*ScreenManager\.java" checks="LocalVariableName"/>
<suppress files=".*ColourContours\.java" checks="AvoidStarImport"/>
</suppressions>
28 changes: 12 additions & 16 deletions src/main/java/com/chromascape/controller/Controller.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import com.chromascape.utils.core.input.keyboard.VirtualKeyboardUtils;
import com.chromascape.utils.core.input.mouse.VirtualMouseUtils;
import com.chromascape.utils.core.input.remoteinput.Kinput;
import com.chromascape.utils.core.input.remoteinput.RemoteInput;
import com.chromascape.utils.core.screen.window.ProcessManagerFactory;
import com.chromascape.utils.core.screen.window.ScreenManager;
import com.chromascape.utils.core.screen.window.WindowHandler;
import com.chromascape.utils.domain.ocr.Ocr;
import com.chromascape.utils.domain.walker.Walker;
import com.chromascape.utils.domain.zones.ZoneManager;
Expand All @@ -30,7 +30,7 @@ private enum ControllerState {

private ControllerState state;

private Kinput kinput;
private RemoteInput remoteInput;
private VirtualMouseUtils virtualMouseUtils;
private VirtualKeyboardUtils virtualKeyboardUtils;
private ZoneManager zoneManager;
Expand All @@ -51,23 +51,20 @@ public Controller() {
*/
public void init() {
logger.info("Setting up Font masks...");
// Warmup: Pre-load common fonts
try {
Ocr.loadFont("Plain 11");
Ocr.loadFont("Plain 12");
Ocr.loadFont("Bold 12");
} catch (Exception e) {
logger.error("Failed to pre-load fonts during init: {}", e.getMessage());
}
Ocr.loadFont("Plain 11");
Ocr.loadFont("Plain 12");
Ocr.loadFont("Bold 12");

logger.info("Setting up Remote Input Library...");
// Obtain process ID of the target window to initialize input injection
kinput = new Kinput(WindowHandler.getPid(WindowHandler.getTargetWindow()));
remoteInput = new RemoteInput(ProcessManagerFactory.getProcessManager().getPid());
// Give screen manager access to Remote Input to grab screen buffer
ScreenManager.setRemoteInput(remoteInput);

// Initialize virtual input utilities with current window bounds and fullscreen status
logger.info("Initialising mouse and keyboard utils...");
virtualMouseUtils = new VirtualMouseUtils(kinput, ScreenManager.getWindowBounds());
virtualKeyboardUtils = new VirtualKeyboardUtils(kinput);
virtualMouseUtils = new VirtualMouseUtils(remoteInput);
virtualKeyboardUtils = new VirtualKeyboardUtils(remoteInput);

logger.info("Pre-loading and instantiating zones...");
// Initialize zone management with fixed mode option
Expand All @@ -89,8 +86,7 @@ public void init() {
* utilities until re-initialized.
*/
public void shutdown() {
mouse().getMouseOverlay().eraseOverlay();
kinput.destroy();
remoteInput.close();
state = ControllerState.STOPPED;
logger.info("Shutting down");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ protected void cycle() {
if (!checkIfCorrectInventoryLayout()) {
logger.warn("Fly-fishing rod must be in inventory slot 27 / idx 26");
logger.warn("Feathers must be in inventory slot 28 / idx 27");
logger.info("The top of your bait or feather images should be cropped by 10 px");
logger.info("The top of feather image should be cropped by 10 px");
stop();
}

Expand Down
9 changes: 5 additions & 4 deletions src/main/java/com/chromascape/scripts/DemoWineScript.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.chromascape.utils.core.screen.window.ScreenManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand Down Expand Up @@ -86,19 +87,19 @@ protected void cycle() {
* keyboard controller.
*/
private void pressEscape() {
controller().keyboard().sendModifierKey(401, "esc");
controller().keyboard().sendKeyDown(KeyEvent.VK_ESCAPE);
waitRandomMillis(80, 100);
controller().keyboard().sendModifierKey(402, "esc");
controller().keyboard().sendKeyRelease(KeyEvent.VK_ESCAPE);
}

/**
* Simulates pressing the Space key by sending the key press and release events to the client
* keyboard controller.
*/
private void pressSpace() {
controller().keyboard().sendModifierKey(401, "space");
controller().keyboard().sendKeyDown(KeyEvent.VK_SPACE);
waitRandomMillis(300, 500);
controller().keyboard().sendModifierKey(402, "space");
controller().keyboard().sendKeyDown(KeyEvent.VK_SPACE);
}

/**
Expand Down
12 changes: 4 additions & 8 deletions src/main/java/com/chromascape/utils/actions/ItemDropper.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.chromascape.utils.core.input.distribution.ClickDistribution;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand All @@ -19,11 +20,6 @@
public class ItemDropper {

private static final Logger logger = LogManager.getLogger(ItemDropper.class);

// AWT Event IDs for Key Press/Release
private static final int KEY_PRESS = 401;
private static final int KEY_RELEASE = 402;

private static final int INVENTORY_SIZE = 28;

/** Defines the order in which items should be dropped. */
Expand Down Expand Up @@ -68,8 +64,8 @@ public static void dropAll(BaseScript baseScript, DropPattern pattern, int[] exc
List<Integer> slotsToDrop = generateSlotIndices(pattern);

// Start Shift-Drop
baseScript.controller().keyboard().sendModifierKey(KEY_PRESS, "shift");
BaseScript.waitRandomMillis(100, 250);
baseScript.controller().keyboard().sendKeyDown(KeyEvent.VK_SHIFT);
BaseScript.waitRandomMillis(400, 850);

try {
for (int slotIndex : slotsToDrop) {
Expand All @@ -90,7 +86,7 @@ public static void dropAll(BaseScript baseScript, DropPattern pattern, int[] exc
}
} finally {
BaseScript.waitRandomMillis(100, 200);
baseScript.controller().keyboard().sendModifierKey(KEY_RELEASE, "shift");
baseScript.controller().keyboard().sendKeyRelease(KeyEvent.VK_SHIFT);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,104 +1,119 @@
package com.chromascape.utils.core.input.keyboard;

import com.chromascape.base.BaseScript;
import com.chromascape.utils.core.input.remoteinput.Kinput;
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.event.KeyEvent;
import java.util.Random;

/**
* Provides high-level methods for simulating keyboard input using the Kinput API. This utility
* supports sending typed characters, modifier keys (e.g., Shift, Ctrl), and directional keys (arrow
* keys), with synchronized access for safe use in multithreaded environments.
* Provides high-level methods for simulating keyboard input using the RemoteInput API. The user can
* use {@link KeyEvent} objects to dictate exactly what to press and for how long. There is
* functionality to type out a string, compensating for modifier keys where necessary.
*/
public class VirtualKeyboardUtils {

private final Kinput kinput;
private final RemoteInput input;

private static final Random RANDOM = new Random();

/**
* Constructs a VirtualKeyboardUtils instance that wraps a Kinput instance.
* Constructs a VirtualKeyboardUtils instance that wraps a RemoteInput instance.
*
* @param kinput The Kinput backend used to emit keyboard events to the target window.
* @param input The RemoteInput object that can operate IO
*/
public VirtualKeyboardUtils(Kinput kinput) {
this.kinput = kinput;
public VirtualKeyboardUtils(RemoteInput input) {
this.input = input;
}

/**
* Sends a keyboard event for a character key (e.g., letters, numbers, symbols). Intended for use
* with regular text input simulation.
*
* @param keyChar The character key to send.
* Updates the state of the bot for the {@link BaseScript}'s stop() function and updates the
* BotState for the UI.
*/
public synchronized void sendKeyChar(char keyChar) {
private void prepareInput() {
BaseScript.checkInterrupted();
StateManager.setState(BotState.ACTING);
StatisticsManager.incrementInputs();
kinput.sendCharEvent(keyChar);
}

/**
* Updates the necessary states of the bot for the {@link BaseScript}'s stop() function and
* updates the BotState for the UI.
* Sends a key down, given that it isn't already 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.
*
* <p>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.
* <p>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);
}
}
}
Loading
Loading