diff --git a/.gitignore b/.gitignore index 87ddbdd..0a46005 100644 --- a/.gitignore +++ b/.gitignore @@ -62,8 +62,8 @@ Thumbs.db output/* !output/.gitkeep -# chromascape runtime cache -.chromascape/ +# chromascape secrets for individual developers +secrets.properties # custom user scripts /src/main/java/com/chromascape/scripts/*/ diff --git a/CVTemplates.sh b/CVTemplates.sh deleted file mode 100644 index 764c4ef..0000000 --- a/CVTemplates.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash - -echo "Cloning SRL-Development repo to extract fonts..." - -# Create fonts directory if it doesn't exist -if [ ! -d "src/main/resources/fonts" ]; then - mkdir -p "src/main/resources/fonts" -fi - -# Create UI images directory if it doesn't exist -if [ ! -d "src/main/resources/images/ui" ]; then - mkdir -p "src/main/resources/images/ui" -fi - -# Clone the repo into a temp folder -git clone --depth 1 https://github.com/Villavu/SRL-Development.git temp_srl_fonts - -# Copy font files -cp -r temp_srl_fonts/fonts/* src/main/resources/fonts/ 2>/dev/null || true - -# Clean up -rm -rf temp_srl_fonts - -echo "Fonts copied to src/main/resources/fonts/" - -# Download UI images from OSBC repo -UI_DIR="src/main/resources/images/ui" - -echo "Downloading UI images..." - -curl -o "$UI_DIR/chat.png" https://raw.githubusercontent.com/kelltom/OS-Bot-COLOR/main/src/images/bot/ui_templates/chat.png -curl -o "$UI_DIR/inv.png" https://raw.githubusercontent.com/kelltom/OS-Bot-COLOR/main/src/images/bot/ui_templates/inv.png -curl -o "$UI_DIR/minimap.png" https://raw.githubusercontent.com/kelltom/OS-Bot-COLOR/main/src/images/bot/ui_templates/minimap.png -curl -o "$UI_DIR/minimap_fixed.png" https://raw.githubusercontent.com/kelltom/OS-Bot-COLOR/main/src/images/bot/ui_templates/minimap_fixed.png - -# Download compass degree images from RuneDark repo -echo "Cloning RuneDark repo to extract compass degrees..." - -# Create compass directory if it doesn't exist -if [ ! -d "src/main/resources/images/ui/compass_degrees" ]; then - mkdir -p "src/main/resources/images/ui/compass_degrees" -fi - -# Clone the repo into a temp folder -git clone --depth 1 https://github.com/cemenenkoff/runedark-public.git temp_runedark - -# Copy compass degree images -cp -r temp_runedark/src/img/bot/ui_templates/compass_degrees/* src/main/resources/images/ui/compass_degrees/ 2>/dev/null || true - -# Clean up -rm -rf temp_runedark - -echo "Compass degree images copied to src/main/resources/images/ui/compass_degrees/" -echo "UI images downloaded to $UI_DIR" - -# Now generate .index files for fonts -FONT_DIR="src/main/resources/fonts" - -echo "Generating index files in $FONT_DIR..." - -# Loop through each directory in fonts -for font_path in "$FONT_DIR"/*; do - if [ -d "$font_path" ]; then - font_name=$(basename "$font_path") - echo "Writing index for \"$font_name\"..." - - # Create index file with list of .bmp files - find "$font_path" -name "*.bmp" -exec basename {} \; > "$font_path/$font_name.index" - fi -done - -echo "Done generating index files." diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index 81bb9c2..781e836 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -12,4 +12,5 @@ + diff --git a/src/main/java/com/chromascape/api/DiscordNotification.java b/src/main/java/com/chromascape/api/DiscordNotification.java new file mode 100644 index 0000000..49d0d43 --- /dev/null +++ b/src/main/java/com/chromascape/api/DiscordNotification.java @@ -0,0 +1,78 @@ +package com.chromascape.api; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides functionality to send logs as notifications to yourself via Discord. + * + *
    + *
  • Loads the {@code secrets.properties} file in the root directory. + *
  • Saves the specified WebHook URL. + *
  • Sends a POST req to the endpoint with the user's desired notification. + *
+ * + *

This is extremely useful if you aren't actively babysitting your bot or in the case of + * reaching a specified XP/GP goal. It's my personal advice to the reader for you to inform yourself + * upon catastrophic failure, promptly. + */ +public class DiscordNotification { + + private static final Logger logger = LoggerFactory.getLogger(DiscordNotification.class); + private static String webhookUrl; + + static { + try (InputStream input = new FileInputStream("secrets.properties")) { + Properties prop = new Properties(); + prop.load(input); + webhookUrl = prop.getProperty("discord.webhook.url"); + } catch (IOException ex) { + logger.info("Could not find secrets.properties in the project root."); + } + } + + /** + * Sends a user specified message to a Discord WebHook endpoint. Sets up a post request and + * expects a 204 response code for success. + * + * @param message User specified String to send to the endpoint. + */ + public static void send(String message) { + if (webhookUrl == null || webhookUrl.isEmpty()) { + return; + } + + String sanitizedMessage = message.replace("\"", "\\\"").replace("\n", "\\n"); + String jsonPayload = "{\"content\": \"" + sanitizedMessage + "\"}"; + + try { + URL url = new URL(webhookUrl); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("User-Agent", "Java-Discord-Webhook"); + + try (OutputStream os = conn.getOutputStream()) { + os.write(jsonPayload.getBytes(StandardCharsets.UTF_8)); + } + + // 204 means Success (No Content) + if (conn.getResponseCode() != 204) { + logger.error("Failed to send. Response code: {}", conn.getResponseCode()); + } + + conn.disconnect(); + } catch (Exception e) { + logger.error("Error sending Discord notification: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/chromascape/base/BaseScript.java b/src/main/java/com/chromascape/base/BaseScript.java index a3e3dee..7341b11 100644 --- a/src/main/java/com/chromascape/base/BaseScript.java +++ b/src/main/java/com/chromascape/base/BaseScript.java @@ -1,7 +1,6 @@ package com.chromascape.base; import com.chromascape.controller.Controller; -import com.chromascape.utils.core.runtime.HotkeyListener; import com.chromascape.utils.core.runtime.ScriptStoppedException; import com.chromascape.utils.core.state.BotState; import com.chromascape.utils.core.state.StateManager; @@ -13,38 +12,35 @@ /** * Abstract base class representing a generic automation script with lifecycle management. * - *

Provides a timed execution framework where the script runs cycles until a specified duration - * elapses or the script is stopped externally. + *

Provides a timed execution framework where the script runs cycles until the script is stopped + * externally. * - *

Manages the underlying Controller instance and logging. Subclasses should override {@link - * #cycle()} to define the script's main logic. + *

Manages the underlying Controller instance. Subclasses should override {@link #cycle()} to + * define the script's main logic. */ public abstract class BaseScript { private final Controller controller; private static final Logger logger = LogManager.getLogger(BaseScript.class); - private final HotkeyListener hotkeyListener; private volatile boolean running = true; private Thread scriptThread; /** Constructs a BaseScript. */ public BaseScript() { controller = new Controller(); - hotkeyListener = new HotkeyListener(this); } /** * Runs the script lifecycle. * *

Initializes the controller, logs start and stop events, then continuously invokes the {@link - * #cycle()} method until the specified duration elapses or the script is stopped. Checks for - * thread interruption and stops gracefully if detected. + * #cycle()} method until the script is stopped. Checks for thread interruption and stops + * gracefully if detected. * *

This method blocks until completion. */ public final void run() { scriptThread = Thread.currentThread(); controller.init(); - hotkeyListener.start(); StatisticsManager.reset(); try { @@ -61,7 +57,7 @@ public final void run() { break; } catch (Exception e) { StateManager.setState(BotState.ERROR); - logger.error("Exception in cycle: {}", e.getMessage()); + logger.error("Exception in cycle: {}, {}", e.getMessage(), e.getStackTrace()); break; } } @@ -84,7 +80,6 @@ public void stop() { } logger.info("Stop requested"); running = false; - hotkeyListener.stop(); // Interrupt the script thread instead of throwing exception if (scriptThread != null) { diff --git a/src/main/java/com/chromascape/scripts/DemoAgilityScript.java b/src/main/java/com/chromascape/scripts/DemoAgilityScript.java index de86b05..dd468fc 100644 --- a/src/main/java/com/chromascape/scripts/DemoAgilityScript.java +++ b/src/main/java/com/chromascape/scripts/DemoAgilityScript.java @@ -1,20 +1,18 @@ package com.chromascape.scripts; +import com.chromascape.api.DiscordNotification; import com.chromascape.base.BaseScript; +import com.chromascape.utils.actions.Minimap; import com.chromascape.utils.actions.MovingObject; import com.chromascape.utils.actions.PointSelector; -import com.chromascape.utils.core.screen.colour.ColourInstances; import com.chromascape.utils.core.screen.colour.ColourObj; import com.chromascape.utils.core.screen.topology.ColourContours; -import com.chromascape.utils.domain.ocr.Ocr; import java.awt.Point; -import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.io.IOException; import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.Random; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -67,7 +65,7 @@ public class DemoAgilityScript extends BaseScript { private static final Point RESET_TILE = ROOFTOP_RESET_TILES.get("Canifis"); private static final int TIMEOUT_XP_CHANGE = 15; - private static final int TIMEOUT_GREEN_APPEAR = 10; + private static final int TIMEOUT_OBSTACLE_APPEAR = 10; // Colour Definitions // These are instantiated as final fields to prevent unnecessary memory allocation during cycles @@ -76,17 +74,9 @@ public class DemoAgilityScript extends BaseScript { private static final ColourObj MARK_COLOUR = new ColourObj("red", new Scalar(0, 254, 254, 0), new Scalar(1, 255, 255, 0)); - // Cached OCR colour object to reduce lookup overhead in repetitive loops - private static ColourObj TEXT_COLOUR_WHITE = null; - // Random used in randomising break times between obstacles private final Random random = new Random(); - /** Initializes the script and pre-loads necessary colour instances. */ - public DemoAgilityScript() { - TEXT_COLOUR_WHITE = ColourInstances.getByName("White"); - } - /** * The main execution loop of the script. * @@ -101,13 +91,29 @@ public DemoAgilityScript() { */ @Override protected void cycle() { - String previousXp = getXp(); + // Log the current XP before clicking obstacle for comparison later + // The idea is to click the obstacle then wait for XP change then loop + int previousXp = -1; + try { + // Read XP + previousXp = Minimap.getXp(this); + // Make sure it's read properly + if (previousXp == -1) { + stop(); + DiscordNotification.send("Xp could not be read."); + } + } catch (IOException e) { + logger.error(e); + stop(); + DiscordNotification.send( + "Bot couldn't read XP bar because of OCR font library load, stopping and logging :("); + } // Check the state of the course if (!isObstacleVisible()) { if (handleMarkOrLost()) { // If we clicked a mark, wait for the course to reset and the Green highlight to appear - waitForObstacleToAppear(TIMEOUT_GREEN_APPEAR); + waitForObstacleToAppear(); return; } } @@ -119,10 +125,12 @@ protected void cycle() { } catch (Exception e) { logger.error("Mouse movement interrupted while clicking moving object: {}", e.getMessage()); stop(); + DiscordNotification.send( + "Mouse movement interrupted while clicking moving object: " + e.getMessage()); } // Wait for the action to complete via XP update - waitUntilXpChange(previousXp, TIMEOUT_XP_CHANGE); + waitUntilXpChange(previousXp); // Humanizing sleep to mimic natural player behavior // And to prevent overloading moving object logic @@ -151,7 +159,7 @@ private boolean handleMarkOrLost() { waitRandomMillis(600, 800); if (!isObstacleVisible()) { try { - logger.info("Lost detected. Walking to reset tile."); + logger.info("We are lost. Walking to reset tile."); controller().walker().pathTo(RESET_TILE, true); waitRandomMillis(4000, 6000); } catch (Exception e) { @@ -162,21 +170,6 @@ private boolean handleMarkOrLost() { return false; } - /** - * Extracts the current Total XP from beside the minimap UI element using OCR. - * - * @return the XP string - */ - private String getXp() { - Rectangle xpZone = controller().zones().getMinimap().get("totalXP"); - try { - return Ocr.extractText(xpZone, "Plain 12", TEXT_COLOUR_WHITE, true); - } catch (IOException e) { - logger.error("Images could not be read from disk {}", e.getMessage()); - } - return ""; - } - /** * Scans the game view for the Red colour associated with a Mark of Grace and attempts to click * it. @@ -207,25 +200,24 @@ private boolean clickMarkOfGraceIfPresent() { * Blocks execution until the Total XP value changes or the timeout is reached. * * @param previousXp the XP value captured before the action started - * @param timeoutSeconds the maximum duration to wait in seconds */ - private void waitUntilXpChange(String previousXp, int timeoutSeconds) { - LocalDateTime endTime = LocalDateTime.now().plusSeconds(timeoutSeconds); + private void waitUntilXpChange(int previousXp) { + LocalDateTime endTime = LocalDateTime.now().plusSeconds(TIMEOUT_XP_CHANGE); // Ensure we do not hang if the initial OCR read failed and returned an empty string - while (!previousXp.isEmpty() - && Objects.equals(previousXp, getXp()) - && LocalDateTime.now().isBefore(endTime)) { - waitMillis(300); + try { + while (previousXp == Minimap.getXp(this) && LocalDateTime.now().isBefore(endTime)) { + waitMillis(300); + } + } catch (Exception e) { + logger.error(e); + stop(); + DiscordNotification.send("Bot couldn't read XP bar, stopping"); } } - /** - * Blocks execution until the obstacle highlight appears or the timeout is reached. - * - * @param timeoutSeconds the maximum duration to wait in seconds - */ - private void waitForObstacleToAppear(int timeoutSeconds) { - LocalDateTime endTime = LocalDateTime.now().plusSeconds(timeoutSeconds); + /** Blocks execution until the obstacle highlight appears or the timeout is reached. */ + private void waitForObstacleToAppear() { + LocalDateTime endTime = LocalDateTime.now().plusSeconds(TIMEOUT_OBSTACLE_APPEAR); while (!isObstacleVisible() && LocalDateTime.now().isBefore(endTime)) { waitMillis(300); } diff --git a/src/main/java/com/chromascape/scripts/DemoFishingScript.java b/src/main/java/com/chromascape/scripts/DemoFishingScript.java index e99bf46..dd93d6a 100644 --- a/src/main/java/com/chromascape/scripts/DemoFishingScript.java +++ b/src/main/java/com/chromascape/scripts/DemoFishingScript.java @@ -44,11 +44,6 @@ public class DemoFishingScript extends BaseScript { private static String lastMessage; - /** Constructs a BaseScript. */ - public DemoFishingScript() { - super(); - } - /** * Overridden cycle. Repeats all tasks within, until stop() is called from either the Web UI, or * from within the script. diff --git a/src/main/java/com/chromascape/scripts/DemoMiningScript.java b/src/main/java/com/chromascape/scripts/DemoMiningScript.java index 3c8d5b7..69c3fa8 100644 --- a/src/main/java/com/chromascape/scripts/DemoMiningScript.java +++ b/src/main/java/com/chromascape/scripts/DemoMiningScript.java @@ -31,11 +31,6 @@ public class DemoMiningScript extends BaseScript { private static final Logger logger = LogManager.getLogger(DemoMiningScript.class); private static final String ironOre = "/images/user/Iron_ore.png"; - /** Constructs a new mining script. */ - public DemoMiningScript() { - super(); - } - /** * Executes one cycle of the script logic. * diff --git a/src/main/java/com/chromascape/scripts/DemoWineScript.java b/src/main/java/com/chromascape/scripts/DemoWineScript.java index 554bb21..ede364b 100644 --- a/src/main/java/com/chromascape/scripts/DemoWineScript.java +++ b/src/main/java/com/chromascape/scripts/DemoWineScript.java @@ -36,18 +36,10 @@ public class DemoWineScript extends BaseScript { private boolean bankFlag = true; - /** Constructs a BaseScript. */ - public DemoWineScript() { - super(); - } - /** - * The core logic of the script. - * - *

This method is called repeatedly in a loop by {@link #run()} for the specified duration. - * Subclasses must override this method to implement their specific bot behavior. - * - *

Note: This method is called synchronously on the running thread. + * The core logic of the script. This function will loop repeatedly until {@link #stop()} is + * called. You should avoid putting all your script logic directly inside this function, instead + * split it up into other functions as shown below. */ @Override protected void cycle() { diff --git a/src/main/java/com/chromascape/scripts/Screenshotter.java b/src/main/java/com/chromascape/scripts/Screenshotter.java index 6f48a40..3617923 100644 --- a/src/main/java/com/chromascape/scripts/Screenshotter.java +++ b/src/main/java/com/chromascape/scripts/Screenshotter.java @@ -9,10 +9,11 @@ import org.apache.logging.log4j.Logger; /** - * Creates screenshots of the client and stores it in an external folder "screenshots". Screenshots - * are saved as "original.png". Screenshots taken by this class are used by the colour picker in the - * web UI. This is a single cycle program that exits out immediately after completion. Served as a - * "user script" in the web UI. + * Creates screenshots of the client and stores it in an external folder "/output". Screenshots are + * saved as "original.png". Screenshots taken by this class are used by the colour picker in the web + * UI. This is a single cycle program that exits out immediately after completion. Served as a "user + * script" in the web UI, however started via a dedicated button, rather than started as a normal + * script. */ public class Screenshotter extends BaseScript { @@ -30,9 +31,9 @@ public Screenshotter() { } /** - * Takes a screenshot and saves it in the "screenshots" folder outside the project directory. - * Although in the BaseScript - this function is repeated until the specified time duration is met - * - Here because this is a one time task it exits out early by calling "stop()". + * Takes a screenshot and saves it in the "/output" directory on the same level as /src. Although + * in the BaseScript - this function is repeated until the specified time duration is met - Here, + * because this is a one time task it exits out early by calling "stop()". */ @Override protected void cycle() { diff --git a/src/main/java/com/chromascape/utils/actions/PointSelector.java b/src/main/java/com/chromascape/utils/actions/PointSelector.java index a14c7dc..0544459 100644 --- a/src/main/java/com/chromascape/utils/actions/PointSelector.java +++ b/src/main/java/com/chromascape/utils/actions/PointSelector.java @@ -227,7 +227,8 @@ private static Point findPointInColourInternal( return null; } - ChromaObj obj = objs.get(0); + // Use the closest object to screen centre since only one object is desired + ChromaObj obj = ColourContours.getChromaObjClosestToCentre(objs); try { int attempts = 0; // Generate initial point using the function provided (Heuristic or Tightness) diff --git a/src/main/java/com/chromascape/utils/core/AccountManager.java b/src/main/java/com/chromascape/utils/core/AccountManager.java deleted file mode 100644 index 32d5554..0000000 --- a/src/main/java/com/chromascape/utils/core/AccountManager.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.chromascape.utils.core; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Singleton class to manage the selected account across the entire application. This provides a - * centralized way to store and access the currently selected account. - */ -public class AccountManager { - - private static final Logger logger = LoggerFactory.getLogger(AccountManager.class); - - // Static variable that persists across all classes - private static String selectedAccount = null; - - // Private constructor to prevent instantiation - private AccountManager() { - throw new UnsupportedOperationException( - "AccountManager is a utility class and cannot be instantiated"); - } - - /** - * Gets the currently selected account. - * - * @return The selected account name, or null if none is selected - */ - public static String getSelectedAccount() { - return selectedAccount; - } - - /** - * Sets the selected account. - * - * @param account The account name to set - */ - public static void setSelectedAccount(String account) { - selectedAccount = account; - logger.info("Selected account updated to: {}", account); - } - - /** Clears the selected account. */ - public static void clearSelectedAccount() { - selectedAccount = null; - logger.info("Selected account cleared"); - } - - /** - * Checks if an account is currently selected. - * - * @return true if an account is selected, false otherwise - */ - public static boolean hasSelectedAccount() { - return selectedAccount != null && !selectedAccount.trim().isEmpty(); - } - - /** - * Gets the selected account or returns a default value if none is selected. - * - * @param defaultValue The default value to return if no account is selected - * @return The selected account or the default value - */ - public static String getSelectedAccountOrDefault(String defaultValue) { - return hasSelectedAccount() ? selectedAccount : defaultValue; - } -} diff --git a/src/main/java/com/chromascape/utils/core/constants/CacheFolderConstants.java b/src/main/java/com/chromascape/utils/core/constants/CacheFolderConstants.java deleted file mode 100644 index 4b53834..0000000 --- a/src/main/java/com/chromascape/utils/core/constants/CacheFolderConstants.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.chromascape.utils.core.constants; - -/** - * The {@code CacheFolderConstants} class defines directory and file naming conventions for the - * ChromaScape cache structure. - * - *

Purpose: Centralizes folder and file names used for local cache organization, - * configuration, logs, scripts, and data files, ensuring consistency across the ChromaScape - * framework. - * - *

Features: - * - *

    - *
  • Top-level cache folder name - *
  • Subdirectory names for config, logs, scripts, data, and cache - *
  • Array of all cache subdirectory names - *
  • Common accounts file name - *
- * - *

These constants are intended for use wherever cache-related paths are constructed or - * referenced within ChromaScape. - * - *

Example usage: - * - *

- *   String configPath =
- *       Path.of(
- *               CacheFolderConstants.CHROMA_CACHE_FOLDER_NAME,
- *               CacheFolderConstants.CONFIG_FOLDER_NAME)
- *           .toString();
- * 
- */ -public class CacheFolderConstants { - public static final String CHROMA_CACHE_FOLDER_NAME = ".chromascape"; - public static final String CONFIG_FOLDER_NAME = "config"; - public static final String LOGS_FOLDER_NAME = "logs"; - public static final String SCRIPTS_FOLDER_NAME = "scripts"; - public static final String DATA_FOLDER_NAME = "data"; - public static final String CACHE_FOLDER_NAME = "cache"; - public static final String[] CHROMA_CACHE_FOLDER_SUBDIRS = { - CONFIG_FOLDER_NAME, LOGS_FOLDER_NAME, SCRIPTS_FOLDER_NAME, DATA_FOLDER_NAME, CACHE_FOLDER_NAME - }; - public static final String ACCOUNTS_FILE_NAME = "accounts.json"; -} diff --git a/src/main/java/com/chromascape/utils/core/input/keyboard/VirtualKeyboardUtils.java b/src/main/java/com/chromascape/utils/core/input/keyboard/VirtualKeyboardUtils.java index fd80672..ce07630 100644 --- a/src/main/java/com/chromascape/utils/core/input/keyboard/VirtualKeyboardUtils.java +++ b/src/main/java/com/chromascape/utils/core/input/keyboard/VirtualKeyboardUtils.java @@ -34,7 +34,17 @@ public synchronized void sendKeyChar(char keyChar) { BaseScript.checkInterrupted(); StateManager.setState(BotState.ACTING); StatisticsManager.incrementInputs(); - kinput.sendKeyEvent(400, keyChar); + kinput.sendCharEvent(keyChar); + } + + /** + * Updates the necessary states of the bot for the {@link BaseScript}'s stop() function and + * updates the BotState for the UI. + */ + private void prepareInput() { + BaseScript.checkInterrupted(); + StateManager.setState(BotState.ACTING); + StatisticsManager.incrementInputs(); } /** @@ -46,9 +56,7 @@ public synchronized void sendKeyChar(char keyChar) { * @throws IllegalArgumentException if the key name is invalid. */ public synchronized void sendModifierKey(int eventId, String key) { - BaseScript.checkInterrupted(); - StateManager.setState(BotState.ACTING); - StatisticsManager.incrementInputs(); + prepareInput(); int keyId = switch (key.toLowerCase()) { case "shift" -> 16; @@ -59,7 +67,19 @@ public synchronized void sendModifierKey(int eventId, String key) { case "space" -> 32; default -> throw new IllegalArgumentException("Invalid modifier key: " + key); }; - kinput.sendModifierKey(eventId, key, keyId); + kinput.sendVirtualKeyEvent(eventId, keyId, key); + } + + /** + * Sends a function key (F1-F12) using the KeyCode slot. + * + * @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). + */ + public synchronized void sendFunctionKey(int eventId, int functionKeyNumber) { + prepareInput(); + int keyId = 111 + functionKeyNumber; // F1 starts at 112 + kinput.sendVirtualKeyEvent(eventId, keyId, "F" + functionKeyNumber); } /** @@ -70,17 +90,15 @@ public synchronized void sendModifierKey(int eventId, String key) { * @throws IllegalArgumentException if the arrow direction is invalid. */ public synchronized void sendArrowKey(int eventId, String key) { - BaseScript.checkInterrupted(); - StateManager.setState(BotState.ACTING); - StatisticsManager.incrementInputs(); + prepareInput(); int keyId = switch (key.toLowerCase()) { case "left" -> 37; - case "right" -> 39; case "up" -> 38; + case "right" -> 39; case "down" -> 40; default -> throw new IllegalArgumentException("Invalid arrow key: " + key); }; - kinput.sendArrowKey(eventId, key, keyId); + kinput.sendVirtualKeyEvent(eventId, keyId, key); } } 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 72034eb..b6afe5d 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 @@ -200,7 +200,7 @@ public void leftClick() { performClick( (p) -> { synchronized (kinputLock) { - kinput.clickLeft(p.x, p.y); + 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); } @@ -212,7 +212,7 @@ public void rightClick() { performClick( (p) -> { synchronized (kinputLock) { - kinput.clickRight(p.x, p.y); + kinput.clickMouse(p.x, p.y, Kinput.MouseButton.RIGHT.id); kinput.moveMouse(p.x, p.y); } }); 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 index fceeda1..b0dfe8f 100644 --- a/src/main/java/com/chromascape/utils/core/input/remoteinput/Kinput.java +++ b/src/main/java/com/chromascape/utils/core/input/remoteinput/Kinput.java @@ -175,11 +175,6 @@ private static KinputInterface loadLibrary() { } } - /** Returns the current system time in milliseconds. */ - private long now() { - return System.currentTimeMillis(); - } - /** Sleeps briefly to simulate human-like click timing. */ private void sleepHumanClick() { try { @@ -196,38 +191,48 @@ private synchronized void focus() { } } - /** Sends a left mouse click at the specified screen coordinate. */ - public synchronized void clickLeft(int x, int y) { - focus(); - if (!kinput.KInput_MouseEvent( - pid, MouseEventType.MOUSE_PRESS.id, now(), 1, x, y, 1, false, MouseButton.LEFT.id)) { - throw new RuntimeException("Left mouse press failed"); - } - sleepHumanClick(); - if (!kinput.KInput_MouseEvent( - pid, MouseEventType.MOUSE_RELEASE.id, now(), 1, x, y, 1, false, MouseButton.LEFT.id)) { - throw new RuntimeException("Left mouse release failed"); - } - } - - /** Sends a right mouse click at the specified screen coordinate. */ - public synchronized void clickRight(int x, int y) { + /** + * 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, now(), 0, x, y, 1, false, MouseButton.RIGHT.id)) { - throw new RuntimeException("Right mouse press failed"); + 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, now(), 0, x, y, 1, false, MouseButton.RIGHT.id)) { - throw new RuntimeException("Right mouse release failed"); + 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, now(), 1, x, y, 1, false, MouseButton.MIDDLE.id)) { + if (!kinput.KInput_MouseEvent( + pid, eventID, System.currentTimeMillis(), 1, x, y, 1, false, MouseButton.MIDDLE.id)) { throw new RuntimeException("Middle mouse event failed"); } } @@ -236,11 +241,27 @@ public synchronized void middleInput(int x, int y, int eventID) { public synchronized void moveMouse(int x, int y) { focus(); if (!kinput.KInput_MouseEvent( - pid, MouseEventType.MOUSE_ENTER.id, now(), 0, x, y, 0, false, MouseButton.NONE.id)) { + 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, now(), 0, x, y, 0, false, MouseButton.NONE.id)) { + pid, + MouseEventType.MOUSE_MOVE.id, + System.currentTimeMillis(), + 0, + x, + y, + 0, + false, + MouseButton.NONE.id)) { throw new RuntimeException("Mouse move failed"); } } @@ -253,34 +274,48 @@ pid, MouseEventType.MOUSE_MOVE.id, now(), 0, x, y, 0, false, MouseButton.NONE.id */ public synchronized void sendKeyEvent(int eventID, char keyChar) { focus(); - if (!kinput.KInput_KeyEvent(pid, eventID, now(), 0, 0, (short) keyChar, 0)) { + if (!kinput.KInput_KeyEvent( + pid, eventID, System.currentTimeMillis(), 0, 0, (short) keyChar, 0)) { throw new RuntimeException("Key event failed for char: " + keyChar); } } - /** Sends a key event for a modifier (e.g. Shift, Ctrl, Alt). */ - public synchronized void sendModifierKey(int eventID, String key, int keyID) { + /** + * Types a character to the client window. + * + * @param keyChar The character to send. + */ + public synchronized void sendCharEvent(char keyChar) { focus(); - if (!kinput.KInput_KeyEvent(pid, eventID, now(), 0, keyID, (short) 0, 0)) { - throw new RuntimeException("Modifier key event failed for key: " + key); + // 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 directional arrow key event. */ - public synchronized void sendArrowKey(int eventID, String key, int keyID) { + /** + * 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(); - if (!kinput.KInput_KeyEvent(pid, eventID, now(), 0, keyID, (short) 0, 0)) { - throw new RuntimeException("Arrow key event failed for key: " + key); + // 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 + * 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"); + throw new RuntimeException("Failed to delete KInput instance"); } } } 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 9eb373f..1f2281d 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 @@ -24,8 +24,8 @@ */ public class ColourContours { - private static final Mat DILATE_KERNEL = getStructuringElement(MORPH_ELLIPSE, new Size(25, 25)); - private static final Mat ERODE_KERNEL = getStructuringElement(MORPH_ELLIPSE, new Size(25, 25)); + private static final Mat DILATE_KERNEL = getStructuringElement(MORPH_ELLIPSE, new Size(20, 20)); + private static final Mat ERODE_KERNEL = getStructuringElement(MORPH_ELLIPSE, new Size(20, 20)); private static final Scalar COLOUR_WHITE = new Scalar(255); private static final Mat EMPTY_HIERARCHY = new Mat(); private static final org.bytedeco.opencv.opencv_core.Point OFFSET_ZERO = @@ -42,11 +42,50 @@ public class ColourContours { public static List getChromaObjsInColour(BufferedImage image, ColourObj colourObj) { Mat mask = extractColours(image, colourObj); morphClose(mask); + ViewportManager.getInstance().updateState(mask); MatVector contours = extractContours(mask); mask.release(); return createChromaObjects(contours); } + /** + * Iterates over a list of ChromaObjs to calculate and return whichever is closest to the + * player/screen centre. Useful in a wide range of activities and preferred over arbitrary choice + * by detection. + * + * @param chromaObjs {@code List} of which to iterate over. + * @return a single {@link ChromaObj} which is closest to the player. + */ + public static ChromaObj getChromaObjClosestToCentre(List chromaObjs) { + if (chromaObjs == null || chromaObjs.isEmpty()) { + return null; + } + + Point screenCentre = + new Point( + (int) ScreenManager.getWindowBounds().getCenterX(), + (int) ScreenManager.getWindowBounds().getCenterY()); + + double minDistance = Double.MAX_VALUE; + ChromaObj closestChromaObj = null; + + for (ChromaObj chromaObj : chromaObjs) { + Point objCentre = + new Point( + (int) chromaObj.boundingBox().getCenterX(), + (int) chromaObj.boundingBox().getCenterY()); + + double currentDistance = objCentre.distance(screenCentre); + + if (currentDistance < minDistance) { + minDistance = currentDistance; + closestChromaObj = chromaObj; + } + } + + return closestChromaObj; + } + /** * Converts the input image to HSV colour space and extracts a binary mask where pixels within the * HSV range specified by the colourObj are white (255), and others are black (0). @@ -80,7 +119,6 @@ public static Mat extractColours(Mat inputMat, ColourObj colourObj) { hsvImage.release(); hsvMin.release(); hsvMax.release(); - ViewportManager.getInstance().updateState(result); return result; } diff --git a/src/main/java/com/chromascape/utils/core/screen/topology/TemplateMatching.java b/src/main/java/com/chromascape/utils/core/screen/topology/TemplateMatching.java index 0cae3d3..f171090 100644 --- a/src/main/java/com/chromascape/utils/core/screen/topology/TemplateMatching.java +++ b/src/main/java/com/chromascape/utils/core/screen/topology/TemplateMatching.java @@ -30,13 +30,10 @@ * *

This class provides a single static method, {@link #match}, which uses the TM_SQDIFF_NORMED * algorithm to locate a template image within a larger base image. It uses an alpha mask to ignore - * transparent pixels in the template, allowing for accurate matching even with irregular shapes. + * transparent pixels in the template. * - *

This is commonly used in ChromaScape to locate UI elements or patterns in the client window, - * based on screen captures and template assets. - * - *

All memory allocated in native OpenCV (Mats and pointers) is explicitly released before - * return. + *

This is commonly to locate UI elements or sprites in the client window, based on screen + * captures and template assets. */ public class TemplateMatching { @@ -56,19 +53,16 @@ public class TemplateMatching { *

The method returns the bounding rectangle of the best match if its matching score is below * the given threshold. If no match satisfies the threshold, the method returns {@code null}. * - *

Note: The method releases the native OpenCV memory of the input and intermediate matrices, - * so the caller should not use the input Mats after this call. - * - * @param templateImg The template image (smaller), expected as a BufferedImage in BGRA format or - * convertible to it. - * @param baseImg The base image (larger) where the template is searched, expected as a - * BufferedImage in BGRA format or convertible to it. + * @param templateImg The template image (smaller), expected as a {@link BufferedImage} in BGRA + * format or convertible to it. + * @param baseImg The base image (larger) where the template is searched, expected as a {@link + * BufferedImage} in BGRA format or convertible to it. * @param threshold The maximum allowed normalized squared difference score for a valid match. * Lower values mean better matches. * @param debugMsg Set this to true if you want detailed messages useful when debugging. * @return A {@link Rectangle} representing the position and size of the matching area in the base * image, or {@code null} if no match meets the threshold criteria. - * @throws IllegalArgumentException If either input Mat is empty (not loaded). + * @throws IllegalArgumentException If either input image is empty (not loaded). * @throws Exception If the template is larger than the base image. */ public static Rectangle match( 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 index 60f2465..8c4f3f8 100644 --- a/src/main/java/com/chromascape/utils/core/screen/window/WindowHandler.java +++ b/src/main/java/com/chromascape/utils/core/screen/window/WindowHandler.java @@ -1,6 +1,5 @@ package com.chromascape.utils.core.screen.window; -import com.chromascape.utils.core.AccountManager; import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.platform.win32.WinDef.HWND; @@ -17,7 +16,7 @@ */ public class WindowHandler { private static final Logger logger = LoggerFactory.getLogger(WindowHandler.class); - private static final String _windowName = "RuneLite"; + private static final String WINDOW_NAME = "RuneLite"; /** * JNA interface for accessing low-level Win32 User32 functions that are not provided by the @@ -79,7 +78,7 @@ public interface User32 extends StdCallLibrary { } /** - * Attempts to locate the window whose title matches the {@code windowName}. + * 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. */ @@ -87,22 +86,13 @@ public static HWND getTargetWindow() { AtomicReference targetHwnd = new AtomicReference<>(); User32 user32 = User32.INSTANCE; - // Get the selected account from AccountManager (loaded during startup) - String selectedAccount = AccountManager.getSelectedAccount(); - - // Build the resolved window name - make it effectively final - final String resolvedWindowName = - (selectedAccount != null && !selectedAccount.trim().isEmpty()) - ? _windowName + " - " + selectedAccount - : _windowName; - user32.EnumWindows( (hwnd, arg) -> { byte[] buffer = new byte[512]; user32.GetWindowTextA(hwnd, buffer, 512); String title = Native.toString(buffer); - if (title.trim().equals(resolvedWindowName)) { + if (title.trim().equals(WINDOW_NAME)) { targetHwnd.set(hwnd); return false; // stop enumeration } diff --git a/src/main/java/com/chromascape/utils/domain/ocr/Ocr.java b/src/main/java/com/chromascape/utils/domain/ocr/Ocr.java index 3b2aabb..0c4186b 100644 --- a/src/main/java/com/chromascape/utils/domain/ocr/Ocr.java +++ b/src/main/java/com/chromascape/utils/domain/ocr/Ocr.java @@ -29,8 +29,6 @@ import java.util.Map; import java.util.Objects; import javax.imageio.ImageIO; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.bytedeco.javacpp.DoublePointer; import org.bytedeco.javacv.Java2DFrameUtils; import org.bytedeco.opencv.opencv_core.Mat; @@ -47,7 +45,7 @@ public class Ocr { /** Stores successful character matches during Ocr extraction. */ private static final List matches = new ArrayList<>(); - /** Cached zero scalar to prevent CPU allocation fatigue */ + /** Cached zero scalar to prevent CPU allocation fatigue. */ private static final Scalar ZERO_SCALAR = new Scalar(0); /** Cache for loaded fonts to prevent disk I/O on every OCR call. */ @@ -58,7 +56,7 @@ public class Ocr { * characters found. */ private static final String ALLOWED_CHARS = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789()[],&-:/*'_\"?"; + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789()[],&-:/*'_\"?<>"; /** * Loads a font glyph set from disk, converts each glyph to grayscale, and stores in a map. Uses diff --git a/src/main/java/com/chromascape/web/config/StartupConfiguration.java b/src/main/java/com/chromascape/web/config/StartupConfiguration.java index 78e1667..4d7b740 100644 --- a/src/main/java/com/chromascape/web/config/StartupConfiguration.java +++ b/src/main/java/com/chromascape/web/config/StartupConfiguration.java @@ -1,12 +1,7 @@ package com.chromascape.web.config; -import com.chromascape.utils.core.AccountManager; -import com.chromascape.utils.core.constants.CacheFolderConstants; import com.chromascape.utils.core.runtime.profile.ProfileManager; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.PostConstruct; -import java.io.File; -import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration; @@ -38,18 +33,12 @@ public void initializeInfrastructure() { try { // Examples: - // - Initialize native libraries (KInput, MinHook) + // - Initialize native libraries (KInput) // - Validate system requirements // - Set up application-wide resources // - Load configuration files // - Perform health checks - // Initialize .chromascape directory - initializeChromascapeDirectory(); - - // Load account configuration - loadAccountConfiguration(); - // Load bot profile config file into RuneLite ProfileManager profileManager = new ProfileManager(); profileManager.loadBotProfile(); @@ -62,115 +51,4 @@ public void initializeInfrastructure() { // if critical infrastructure fails to initialize } } - - /** - * Initializes the .chromascape directory in the project root directory. Creates the directory if - * it doesn't exist and sets up any required subdirectories. - */ - private void initializeChromascapeDirectory() { - try { - // Get project root directory (where build.gradle.kts is located) - String projectRoot = System.getProperty("user.dir"); - File chromascapeDir = new File(projectRoot, CacheFolderConstants.CHROMA_CACHE_FOLDER_NAME); - - // Create .chromascape directory if it doesn't exist - if (!chromascapeDir.exists()) { - boolean created = chromascapeDir.mkdirs(); - if (created) { - logger.info("Created .chromascape directory at: {}", chromascapeDir.getAbsolutePath()); - createSubdirectories(chromascapeDir); - } else { - String message = - String.format( - "Could not create %s directory", CacheFolderConstants.CHROMA_CACHE_FOLDER_NAME); - logger.error(message); - throw new RuntimeException(message); - } - } else { - String message = - String.format( - "Found existing %s directory at: %s", - CacheFolderConstants.CHROMA_CACHE_FOLDER_NAME, chromascapeDir.getAbsolutePath()); - logger.info(message); - } - - // Verify directory is writable - if (!chromascapeDir.canWrite()) { - String message = - String.format( - "Warning: %s directory is not writable", - CacheFolderConstants.CHROMA_CACHE_FOLDER_NAME); - logger.warn(message); - throw new RuntimeException(message); - } - - } catch (Exception e) { - String message = - String.format( - "Error initializing %s directory: %s", - CacheFolderConstants.CACHE_FOLDER_NAME, e.getMessage()); - logger.error(message); - throw new RuntimeException(message, e); - } - } - - /** - * Creates necessary subdirectories within the .chromascape directory. - * - * @param chromascapeDir the main .chromascape directory - */ - private void createSubdirectories(File chromascapeDir) { - for (String subdir : CacheFolderConstants.CHROMA_CACHE_FOLDER_SUBDIRS) { - File subdirFile = new File(chromascapeDir, subdir); - if (!subdirFile.exists()) { - boolean created = subdirFile.mkdir(); - if (created) { - logger.info("Created subdirectory: {}", subdir); - } else { - logger.warn("Warning: Failed to create subdirectory: {}", subdir); - } - } - } - } - - /** - * Loads account configuration from the accounts.json file and populates the AccountManager. This - * method reads the first account from the JSON file and sets it as the selected account. - */ - private void loadAccountConfiguration() { - try { - String projectRoot = System.getProperty("user.dir"); - File accountsFile = - new File( - projectRoot, - CacheFolderConstants.CHROMA_CACHE_FOLDER_NAME - + "/" - + CacheFolderConstants.CONFIG_FOLDER_NAME - + "/" - + CacheFolderConstants.ACCOUNTS_FILE_NAME); - - if (accountsFile.exists()) { - logger.info("Loading account configuration from: {}", accountsFile.getAbsolutePath()); - - // Read the JSON file and parse it as a String array - ObjectMapper objectMapper = new ObjectMapper(); - String[] accounts = objectMapper.readValue(accountsFile, String[].class); - - // Select the first account if any exist - if (accounts.length > 0) { - String firstAccount = accounts[0]; - AccountManager.setSelectedAccount(firstAccount); - logger.info("Account loaded successfully: {}", firstAccount); - } else { - logger.warn("No accounts found in accounts.json file"); - } - } else { - logger.info("No accounts.json file found, no account will be loaded"); - } - } catch (IOException e) { - logger.error("Error reading accounts file: {}", e.getMessage()); - } catch (Exception e) { - logger.error("Unexpected error during account loading: {}", e.getMessage()); - } - } } diff --git a/src/main/resources/static/js/colour.js b/src/main/resources/static/js/colour.js index e13560f..5ac656b 100644 --- a/src/main/resources/static/js/colour.js +++ b/src/main/resources/static/js/colour.js @@ -19,6 +19,7 @@ document.addEventListener("DOMContentLoaded", () => { initSliders(); initSubmitButton(); fetchSliderState(); // Restore state from server + initNameInput(); updateImages().catch(console.error); }); @@ -184,13 +185,7 @@ const pendingUpdates = new Map(); /** * Sends a slider value update to the server using a conflating queue loop. - * - *

This implementation solves the "backend overload" problem by ensuring that only one request - * is ever in flight per slider. If the user moves the slider repeatedly while a request is processing, - * intermediate values are locally overwritten (conflated). As soon as the active request finishes, - * the loop immediately picks up the current latest value and sends it. - * - *

This guarantees the server always receives the final state eventually without lag accumulation. + * To ensure that the slider state doesn't overload the backend. * * @param {string} sliderName - The ID/name of the slider being updated. * @param {string|number} value - The new value of the slider. @@ -283,4 +278,16 @@ function toCamelCase(str) { return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => { return index === 0 ? word.toLowerCase() : word.toUpperCase(); }).replace(/\s+/g, ''); -} \ No newline at end of file +} +/** + * Initializes the colour name input listener to update the code snippet in real-time. + */ +function initNameInput() { + const nameInput = document.getElementById("colourNameInput"); + if (nameInput) { + // 'input' fires on every keystroke, 'change' only when focus is lost + nameInput.addEventListener("input", () => { + updateCodeSnippet(); + }); + } +} diff --git a/src/test/java/com/chromascape/web/config/StartupConfigurationTest.java b/src/test/java/com/chromascape/web/config/StartupConfigurationTest.java deleted file mode 100644 index 9db92ea..0000000 --- a/src/test/java/com/chromascape/web/config/StartupConfigurationTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.chromascape.web.config; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import java.io.File; -import java.lang.reflect.Method; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.slf4j.Logger; -import org.springframework.test.util.ReflectionTestUtils; - -/** Test class for {@link StartupConfiguration}. */ -class StartupConfigurationTest { - - @TempDir File tempDir; - - private StartupConfiguration startupConfiguration; - private static Logger mockLogger; - - @BeforeEach - void setUp() { - startupConfiguration = new StartupConfiguration(); - mockLogger = mock(Logger.class); // This will now create an SLF4J mock - ReflectionTestUtils.setField(startupConfiguration, "logger", mockLogger); - } - - @Test - void testStartupConfigurationCanBeInstantiated() { - assertNotNull(startupConfiguration, "StartupConfiguration should be instantiable"); - } - - @Test - void testInitializeInfrastructureCreatesChromascapeDirectory() throws Exception { - final String originalUserDir = System.getProperty("user.dir"); - System.setProperty("user.dir", tempDir.getAbsolutePath()); - - startupConfiguration.initializeInfrastructure(); - - File expectedDir = new File(tempDir, ".chromascape"); - assertTrue(expectedDir.exists(), ".chromascape directory should be created"); - assertTrue(expectedDir.isDirectory(), ".chromascape should be a directory"); - assertTrue(expectedDir.canWrite(), ".chromascape directory should be writable"); - - verify(mockLogger, atLeastOnce()).info(anyString()); - - System.setProperty("user.dir", originalUserDir); - } - - @Test - void testInitializeInfrastructureCreatesSubdirectories() throws Exception { - final String originalUserDir = System.getProperty("user.dir"); - System.setProperty("user.dir", tempDir.getAbsolutePath()); - - startupConfiguration.initializeInfrastructure(); - - File chromascapeDir = new File(tempDir, ".chromascape"); - String[] expectedSubdirs = {"config", "logs", "scripts", "data", "cache"}; - for (String subdir : expectedSubdirs) { - File subdirFile = new File(chromascapeDir, subdir); - assertTrue(subdirFile.exists(), "Subdirectory " + subdir + " should exist"); - assertTrue(subdirFile.isDirectory(), "Subdirectory " + subdir + " should be a directory"); - } - - System.setProperty("user.dir", originalUserDir); - } - - @Test - void testInitializeInfrastructureHandlesExistingDirectory() throws Exception { - final String originalUserDir = System.getProperty("user.dir"); - System.setProperty("user.dir", tempDir.getAbsolutePath()); - - File existingDir = new File(tempDir, ".chromascape"); - existingDir.mkdirs(); - - startupConfiguration.initializeInfrastructure(); - - assertTrue(existingDir.exists(), "Existing directory should still exist"); - verify(mockLogger, atLeastOnce()).info(anyString()); - - System.setProperty("user.dir", originalUserDir); - } - - @Test - void testInitializeInfrastructureLogsAppropriateMessages() throws Exception { - final String originalUserDir = System.getProperty("user.dir"); - System.setProperty("user.dir", tempDir.getAbsolutePath()); - - startupConfiguration.initializeInfrastructure(); - - verify(mockLogger, atLeastOnce()).info(anyString()); - - System.setProperty("user.dir", originalUserDir); - } - - @Test - void testInitializeInfrastructureHandlesDirectoryCreationFailure() throws Exception { - final String originalUserDir = System.getProperty("user.dir"); - System.setProperty("user.dir", "/root/restricted"); - - try { - startupConfiguration.initializeInfrastructure(); - } catch (RuntimeException e) { - assertTrue(e.getMessage().contains("Failed to initialize .chromascape directory")); - } - - System.setProperty("user.dir", originalUserDir); - } - - @Test - void testCreateSubdirectoriesCreatesAllRequiredSubdirectories() throws Exception { - Method createSubdirectoriesMethod = - StartupConfiguration.class.getDeclaredMethod("createSubdirectories", File.class); - createSubdirectoriesMethod.setAccessible(true); - - File testDir = new File(tempDir, "test-chromascape"); - testDir.mkdirs(); - - createSubdirectoriesMethod.invoke(startupConfiguration, testDir); - - String[] expectedSubdirs = {"config", "logs", "scripts", "data", "cache"}; - for (String subdir : expectedSubdirs) { - File subdirFile = new File(testDir, subdir); - assertTrue(subdirFile.exists(), "Subdirectory " + subdir + " should be created"); - } - } - - @Test - void testCreateSubdirectoriesHandlesExistingSubdirectories() throws Exception { - Method createSubdirectoriesMethod = - StartupConfiguration.class.getDeclaredMethod("createSubdirectories", File.class); - createSubdirectoriesMethod.setAccessible(true); - - File testDir = new File(tempDir, "test-chromascape"); - testDir.mkdirs(); - - File existingSubdir = new File(testDir, "config"); - existingSubdir.mkdirs(); - - createSubdirectoriesMethod.invoke(startupConfiguration, testDir); - - String[] expectedSubdirs = {"config", "logs", "scripts", "data", "cache"}; - for (String subdir : expectedSubdirs) { - File subdirFile = new File(testDir, subdir); - assertTrue(subdirFile.exists(), "Subdirectory " + subdir + " should exist"); - } - } - - @Test - void testInitializeChromascapeDirectoryPrivateMethod() throws Exception { - Method initializeChromascapeDirectoryMethod = - StartupConfiguration.class.getDeclaredMethod("initializeChromascapeDirectory"); - initializeChromascapeDirectoryMethod.setAccessible(true); - - final String originalUserDir = System.getProperty("user.dir"); - System.setProperty("user.dir", tempDir.getAbsolutePath()); - - initializeChromascapeDirectoryMethod.invoke(startupConfiguration); - - File expectedDir = new File(tempDir, ".chromascape"); - assertTrue(expectedDir.exists(), ".chromascape directory should be created"); - - System.setProperty("user.dir", originalUserDir); - } -} diff --git a/third_party/KInput/KInput/KInput/bin/Release/KInput.dll b/third_party/KInput/KInput/KInput/bin/Release/KInput.dll index 71b2c8d..9baca1d 100644 Binary files a/third_party/KInput/KInput/KInput/bin/Release/KInput.dll and b/third_party/KInput/KInput/KInput/bin/Release/KInput.dll differ diff --git a/third_party/KInput/KInput/KInput/makefile b/third_party/KInput/KInput/KInput/makefile index edb2054..5465995 100644 --- a/third_party/KInput/KInput/KInput/makefile +++ b/third_party/KInput/KInput/KInput/makefile @@ -11,14 +11,14 @@ AR = ar.exe LD = g++.exe WINDRES = windres.exe -INC = -I"..\..\..\minhook\include" -CFLAGS = -RESINC = -LIBDIR = -L"..\..\..\minhook\build\MinGW" -LIB = -l"MinHook" +INC = +CFLAGS = +RESINC = +LIBDIR = +LIB = LDFLAGS = -INC_RELEASE = $(INC) -I"C:\Program Files\Microsoft\jdk-17.0.16.8-hotspot\include" -I"C:\Program Files\Microsoft\jdk-17.0.16.8-hotspot\include\win32" +INC_RELEASE = -I"C:\Program Files\Microsoft\jdk-17.0.16.8-hotspot\include" -I"C:\Program Files\Microsoft\jdk-17.0.16.8-hotspot\include\win32" CFLAGS_RELEASE = $(CFLAGS) -Os -Wall -std=c++1z -DBUILD_DLL RESINC_RELEASE = $(RESINC) RCFLAGS_RELEASE = $(RCFLAGS) @@ -35,16 +35,16 @@ all: release clean: clean_release -before_release: +before_release: cmd /c if not exist bin\\Release md bin\\Release cmd /c if not exist $(OBJDIR_RELEASE) md $(OBJDIR_RELEASE) -after_release: +after_release: release: before_release out_release after_release out_release: before_release $(OBJ_RELEASE) $(DEP_RELEASE) - $(LD) -shared $(LIBDIR_RELEASE) $(OBJ_RELEASE) -o $(OUT_RELEASE) $(LDFLAGS_RELEASE) $(LIB_RELEASE) + $(LD) -shared $(OBJ_RELEASE) -o $(OUT_RELEASE) $(LDFLAGS_RELEASE) $(OBJDIR_RELEASE)\\KInput.o: KInput.cpp $(CXX) $(CFLAGS_RELEASE) $(INC_RELEASE) -c KInput.cpp -o $(OBJDIR_RELEASE)\\KInput.o @@ -52,7 +52,7 @@ $(OBJDIR_RELEASE)\\KInput.o: KInput.cpp $(OBJDIR_RELEASE)\\main.o: main.cpp $(CXX) $(CFLAGS_RELEASE) $(INC_RELEASE) -c main.cpp -o $(OBJDIR_RELEASE)\\main.o -clean_release: +clean_release: cmd /c del /f $(OBJ_RELEASE) $(OUT_RELEASE) cmd /c rd bin\\Release cmd /c rd $(OBJDIR_RELEASE) diff --git a/third_party/KInput/KInput/KInput/obj/Release/KInput.o b/third_party/KInput/KInput/KInput/obj/Release/KInput.o index 0c0d86a..780db6c 100644 Binary files a/third_party/KInput/KInput/KInput/obj/Release/KInput.o and b/third_party/KInput/KInput/KInput/obj/Release/KInput.o differ diff --git a/third_party/KInput/KInput/KInput/obj/Release/main.o b/third_party/KInput/KInput/KInput/obj/Release/main.o index 6b36669..7bf5a9f 100644 Binary files a/third_party/KInput/KInput/KInput/obj/Release/main.o and b/third_party/KInput/KInput/KInput/obj/Release/main.o differ diff --git a/third_party/KInput/KInput/KInputCtrl/bin/Release/KInputCtrl.dll b/third_party/KInput/KInput/KInputCtrl/bin/Release/KInputCtrl.dll index 9fee134..eddf38e 100644 Binary files a/third_party/KInput/KInput/KInputCtrl/bin/Release/KInputCtrl.dll and b/third_party/KInput/KInput/KInputCtrl/bin/Release/KInputCtrl.dll differ diff --git a/third_party/KInput/KInput/KInputCtrl/obj/Release/RemoteProcFinder.o b/third_party/KInput/KInput/KInputCtrl/obj/Release/RemoteProcFinder.o index 71a4c66..39ad0e5 100644 Binary files a/third_party/KInput/KInput/KInputCtrl/obj/Release/RemoteProcFinder.o and b/third_party/KInput/KInput/KInputCtrl/obj/Release/RemoteProcFinder.o differ diff --git a/third_party/KInput/KInput/KInputCtrl/obj/Release/main.o b/third_party/KInput/KInput/KInputCtrl/obj/Release/main.o index b5553f6..fbf1c98 100644 Binary files a/third_party/KInput/KInput/KInputCtrl/obj/Release/main.o and b/third_party/KInput/KInput/KInputCtrl/obj/Release/main.o differ