Skip to content

Commit 88b7b5f

Browse files
authored
Add DemoAgility, moving object support and tightness overload (#52)
- Add new `DemoAgilityScript` - This script was tested at Draynor, Varrock, and Canifis and has Reset tiles for each (Configuration needed) - Picks up marks - Add MovingObject class - Finds the object onscreen - Clicks it - Uses concurrency to retry until a red click event ocurrs - Add overloads for a new "tightness" parameter - The higher the tightness parameter -> the closer to the centre the click location distribution - You'll find this parameter inside `PointSelector` and `ClickDistribution` - Essential for ground items - Update `CVTemplates.bat` to include red click event images from OSBC - These are the images that we search for when looking for a red click - Remove destruction of KInput in controller - After testing, I verified that it wasn't working
1 parent a87899d commit 88b7b5f

File tree

11 files changed

+703
-110
lines changed

11 files changed

+703
-110
lines changed

CVTemplates.bat

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ curl -o "%UI_DIR%\inv.png" https://raw.githubusercontent.com/kelltom/OS-Bot-COLO
3232
curl -o "%UI_DIR%\minimap.png" https://raw.githubusercontent.com/kelltom/OS-Bot-COLOR/main/src/images/bot/ui_templates/minimap.png
3333
curl -o "%UI_DIR%\minimap_fixed.png" https://raw.githubusercontent.com/kelltom/OS-Bot-COLOR/main/src/images/bot/ui_templates/minimap_fixed.png
3434

35+
REM Download Mouse Click images
36+
set "MOUSE_DIR=src\main\resources\images\mouse_clicks"
37+
38+
if not exist "%MOUSE_DIR%" (
39+
mkdir "%MOUSE_DIR%"
40+
)
41+
42+
echo Downloading Mouse Click images to %MOUSE_DIR%...
43+
44+
curl -o "%MOUSE_DIR%\red_1.png" https://raw.githubusercontent.com/kelltom/OS-Bot-COLOR/main/src/images/bot/mouse_clicks/red_1.png
45+
curl -o "%MOUSE_DIR%\red_2.png" https://raw.githubusercontent.com/kelltom/OS-Bot-COLOR/main/src/images/bot/mouse_clicks/red_2.png
46+
curl -o "%MOUSE_DIR%\red_3.png" https://raw.githubusercontent.com/kelltom/OS-Bot-COLOR/main/src/images/bot/mouse_clicks/red_3.png
47+
curl -o "%MOUSE_DIR%\red_4.png" https://raw.githubusercontent.com/kelltom/OS-Bot-COLOR/main/src/images/bot/mouse_clicks/red_4.png
48+
49+
echo Mouse clicks downloaded.
50+
3551
REM Download compass degree images from RuneDark repo
3652
echo Cloning RuneDark repo to extract compass degrees...
3753

@@ -72,4 +88,4 @@ for /D %%D in ("%FONT_DIR%\*") do (
7288
)
7389

7490
endlocal
75-
echo Done generating index files.
91+
echo Done generating index files.

src/main/java/com/chromascape/controller/Controller.java

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -91,46 +91,10 @@ public void init() {
9191
public void shutdown() {
9292
mouse().getMouseOverlay().eraseOverlay();
9393
kinput.destroy();
94-
if (!killKinput()) {
95-
logger.warn("Kinput failed to destroy");
96-
}
9794
state = ControllerState.STOPPED;
9895
logger.info("Shutting down");
9996
}
10097

101-
/**
102-
* Uses the command prompt to forcibly delete KInput.dll and KInputCtrl.dll from the build
103-
* directory. Effectively freeing up the program to rerun.
104-
*
105-
* @return {@code true} if successful, {@code false} if not.
106-
*/
107-
public boolean killKinput() {
108-
try {
109-
String distPath = new java.io.File("build/dist").getAbsolutePath();
110-
String kinputCtrl = "\"" + distPath + "\\KInputCtrl.dll\"";
111-
String kinput = "\"" + distPath + "\\KInput.dll\"";
112-
113-
// Wait 2s, then force delete files.
114-
// We use cmd /c start "" /B to ensure it runs detached/background if possible.
115-
// Using ProcessBuilder to avoid Runtime.exec deprecation.
116-
new ProcessBuilder(
117-
"cmd.exe",
118-
"/c",
119-
"start",
120-
"/MIN",
121-
"cmd.exe",
122-
"/c",
123-
"timeout /t 2 /nobreak > NUL & del /f /q " + kinputCtrl + " " + kinput)
124-
.start();
125-
126-
logger.info("Scheduled forced Kinput DLL cleanup in 2 seconds.");
127-
return true;
128-
} catch (Exception e) {
129-
logger.error("Failed to schedule Kinput cleanup: {}", e.getMessage());
130-
return false;
131-
}
132-
}
133-
13498
/**
13599
* Provides access to the virtual mouse utility.
136100
*
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package com.chromascape.scripts;
2+
3+
import com.chromascape.base.BaseScript;
4+
import com.chromascape.utils.actions.MovingObject;
5+
import com.chromascape.utils.actions.PointSelector;
6+
import com.chromascape.utils.core.screen.colour.ColourInstances;
7+
import com.chromascape.utils.core.screen.colour.ColourObj;
8+
import com.chromascape.utils.core.screen.topology.ColourContours;
9+
import com.chromascape.utils.domain.ocr.Ocr;
10+
import java.awt.Point;
11+
import java.awt.Rectangle;
12+
import java.awt.image.BufferedImage;
13+
import java.io.IOException;
14+
import java.time.LocalDateTime;
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
import java.util.Objects;
18+
import java.util.Random;
19+
import org.apache.logging.log4j.LogManager;
20+
import org.apache.logging.log4j.Logger;
21+
import org.bytedeco.opencv.opencv_core.Scalar;
22+
23+
/**
24+
* An Agility script designed to be used with the ChromaScape RuneLite plugin configuration.
25+
*
26+
* <p>>>>TO USE THIS SCRIPT YOU MUST ENABLE THE PERMANENT XP BAR<<<
27+
*
28+
* <p>See instructions here: <a
29+
* href="https://github.com/StaticSweep/ChromaScape/wiki/Requirements">Instructions</a>
30+
*
31+
* <p>The script relies on the improved agility plugin to show only the next visible obstacle or
32+
* mark.
33+
*
34+
* <ul>
35+
* <li>Green Highlight indicates the next obstacle is safe to click
36+
* <li>Red Highlight (or absence of Green) indicates a Mark of Grace (next obstacle isn't
37+
* highlighted green because of the plugin)
38+
* <li>This script uses concurrency (multiple threads) to click the obstacle until a red click
39+
* occurs.
40+
* </ul>
41+
*
42+
* <p>This implementation prioritizes Mark of Grace collection over course progression and includes
43+
* fail-safe logic to prevent getting confused during the delay between looting and the plugin
44+
* updating the obstacle highlights.
45+
*/
46+
public class DemoAgilityScript extends BaseScript {
47+
48+
// Logger that appends to the Web UI
49+
private static final Logger logger = LogManager.getLogger(DemoAgilityScript.class);
50+
51+
// Preset tiles for specific rooftop courses
52+
private static final Map<String, Point> ROOFTOP_RESET_TILES =
53+
new HashMap<>() {
54+
{
55+
put("Draynor", new Point(3103, 3278));
56+
put("Varrock", new Point(3223, 3414));
57+
put("Canifis", new Point(3507, 3487));
58+
}
59+
};
60+
61+
// Configuration Constants
62+
63+
/**
64+
* This is where you pick the reset tile for your script. e.g., if using Varrock, set the String
65+
* below to "Varrock"
66+
*/
67+
private static final Point RESET_TILE = ROOFTOP_RESET_TILES.get("Canifis");
68+
69+
private static final int TIMEOUT_XP_CHANGE = 15;
70+
private static final int TIMEOUT_GREEN_APPEAR = 10;
71+
72+
// Colour Definitions
73+
// These are instantiated as final fields to prevent unnecessary memory allocation during cycles
74+
private static final ColourObj OBSTACLE_COLOUR =
75+
new ColourObj("green", new Scalar(59, 254, 254, 0), new Scalar(60, 255, 255, 0));
76+
private static final ColourObj MARK_COLOUR =
77+
new ColourObj("red", new Scalar(0, 254, 254, 0), new Scalar(1, 255, 255, 0));
78+
79+
// Cached OCR colour object to reduce lookup overhead in repetitive loops
80+
private static ColourObj TEXT_COLOUR_WHITE = null;
81+
82+
// Random used in randomising break times between obstacles
83+
private final Random random = new Random();
84+
85+
/** Initializes the script and pre-loads necessary colour instances. */
86+
public DemoAgilityScript() {
87+
TEXT_COLOUR_WHITE = ColourInstances.getByName("White");
88+
}
89+
90+
/**
91+
* The main execution loop of the script.
92+
*
93+
* <p>The cycle follows a priority order:
94+
*
95+
* <ul>
96+
* <li>Check for the next obstacle highlight and mark of grace as a fallback
97+
* <li>If present, click it and wait for xp or pickup
98+
* <li>If neither is present, verify state and potentially walk to reset
99+
* <li>1% chance of taking a break after each obstacle click
100+
* </ul>
101+
*/
102+
@Override
103+
protected void cycle() {
104+
String previousXp = getXp();
105+
106+
// Check the state of the course
107+
if (!isObstacleVisible()) {
108+
if (handleMarkOrLost()) {
109+
// If we clicked a mark, wait for the course to reset and the Green highlight to appear
110+
waitForObstacleToAppear(TIMEOUT_GREEN_APPEAR);
111+
return;
112+
}
113+
}
114+
115+
// Interact with the detected obstacle
116+
// Clicking continuously until the Red X animation is detected
117+
try {
118+
MovingObject.clickMovingObjectByColourObjUntilRedClick(OBSTACLE_COLOUR, this);
119+
} catch (Exception e) {
120+
logger.error("Mouse movement interrupted while clicking moving object: {}", e.getMessage());
121+
stop();
122+
}
123+
124+
// Wait for the action to complete via XP update
125+
waitUntilXpChange(previousXp, TIMEOUT_XP_CHANGE);
126+
127+
// Humanizing sleep to mimic natural player behavior
128+
// And to prevent overloading moving object logic
129+
waitRandomMillis(650, 800);
130+
131+
// 1% chance to take a break between 2 and 5 minutes after clicking an obstacle
132+
if (random.nextInt(100) < 1) {
133+
logger.info("Taking a break...");
134+
waitRandomMillis(120000, 300000);
135+
}
136+
}
137+
138+
/**
139+
* Manages the scenario when the agility obstacle is not visible. It first tries to find a Mark of
140+
* Grace. If no mark is found, it enters a fail-safe check to confirm the player is truly lost
141+
* before finally attempting to walk to the RESET tile.
142+
*
143+
* @return true if a Mark of Grace was successfully clicked, false otherwise
144+
*/
145+
private boolean handleMarkOrLost() {
146+
if (clickMarkOfGraceIfPresent()) {
147+
return true;
148+
}
149+
150+
// Double check we are actually lost to protect against lag or rendering delays
151+
waitRandomMillis(600, 800);
152+
if (!isObstacleVisible()) {
153+
try {
154+
logger.info("Lost detected. Walking to reset tile.");
155+
controller().walker().pathTo(RESET_TILE, true);
156+
waitRandomMillis(4000, 6000);
157+
} catch (Exception e) {
158+
logger.error("Walker error {}", e.getMessage());
159+
stop();
160+
}
161+
}
162+
return false;
163+
}
164+
165+
/**
166+
* Extracts the current Total XP from beside the minimap UI element using OCR.
167+
*
168+
* @return the XP string
169+
*/
170+
private String getXp() {
171+
Rectangle xpZone = controller().zones().getMinimap().get("totalXP");
172+
try {
173+
return Ocr.extractText(xpZone, "Plain 12", TEXT_COLOUR_WHITE, true);
174+
} catch (IOException e) {
175+
logger.error("Images could not be read from disk {}", e.getMessage());
176+
}
177+
return "";
178+
}
179+
180+
/**
181+
* Scans the game view for the Red colour associated with a Mark of Grace and attempts to click
182+
* it.
183+
*
184+
* @return true if the mouse action was taken, false if no mark was found
185+
*/
186+
private boolean clickMarkOfGraceIfPresent() {
187+
BufferedImage gameView = controller().zones().getGameView();
188+
// You'll see that there's an extra parameter on the point selector
189+
// This is "tightness", how closely grouped the click should be
190+
// 15.0 or more works best for ground items, best to look from a higher camera angle
191+
Point clickLocation = PointSelector.getRandomPointByColourObj(gameView, MARK_COLOUR, 15, 15.0);
192+
193+
if (clickLocation != null) {
194+
try {
195+
controller().mouse().moveTo(clickLocation, "medium");
196+
controller().mouse().leftClick();
197+
return true;
198+
} catch (Exception e) {
199+
logger.error("Mouse failed while moving to mark of grace {}", e.getMessage());
200+
stop();
201+
}
202+
}
203+
return false;
204+
}
205+
206+
/**
207+
* Blocks execution until the Total XP value changes or the timeout is reached.
208+
*
209+
* @param previousXp the XP value captured before the action started
210+
* @param timeoutSeconds the maximum duration to wait in seconds
211+
*/
212+
private void waitUntilXpChange(String previousXp, int timeoutSeconds) {
213+
LocalDateTime endTime = LocalDateTime.now().plusSeconds(timeoutSeconds);
214+
// Ensure we do not hang if the initial OCR read failed and returned an empty string
215+
while (!previousXp.isEmpty()
216+
&& Objects.equals(previousXp, getXp())
217+
&& LocalDateTime.now().isBefore(endTime)) {
218+
waitMillis(300);
219+
}
220+
}
221+
222+
/**
223+
* Blocks execution until the obstacle highlight appears or the timeout is reached.
224+
*
225+
* @param timeoutSeconds the maximum duration to wait in seconds
226+
*/
227+
private void waitForObstacleToAppear(int timeoutSeconds) {
228+
LocalDateTime endTime = LocalDateTime.now().plusSeconds(timeoutSeconds);
229+
while (!isObstacleVisible() && LocalDateTime.now().isBefore(endTime)) {
230+
waitMillis(300);
231+
}
232+
}
233+
234+
/**
235+
* Checks if the obstacle highlight is currently present in the game view.
236+
*
237+
* @return true if the colour contours are detected, false otherwise
238+
*/
239+
private boolean isObstacleVisible() {
240+
BufferedImage gameView = controller().zones().getGameView();
241+
return !ColourContours.getChromaObjsInColour(gameView, OBSTACLE_COLOUR).isEmpty();
242+
}
243+
}

0 commit comments

Comments
 (0)