diff --git a/.gitignore b/.gitignore index b2a7d128..f523d878 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,24 @@ target/* .lsp/* output/* + +# Claude Code +.claude/ + +# Debug / decompilation artifacts +*.log +data/ +RuneLite_parent_decompiled.java +clientui_bytecode.txt +clientui_decompile.txt +clientui_out.txt +rl_decompile_full.txt +rl_err.txt +rl_full.txt +runelite_bytecode.txt +runelite_decompile.txt +runelite_err.txt +runelite_parent_bytecode.txt + +# Scripts +start-script.sh diff --git a/OSRSBot.jar b/OSRSBot.jar index a332d848..fd4cfc9c 100644 Binary files a/OSRSBot.jar and b/OSRSBot.jar differ diff --git a/build.gradle b/build.gradle index e2ce63f3..915d4410 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,9 @@ dependencies { implementation group: 'org.clojure', name: 'clojure', version: '1.9.0' implementation group: 'org.clojure', name: 'tools.nrepl', version: '0.2.12' implementation group: 'net.runelite', name: 'client', version: runeLiteVersion - implementation group: 'net.runelite', name: 'cache', version: runeLiteVersion + implementation(group: 'net.runelite', name: 'cache', version: runeLiteVersion) { + exclude group: 'org.slf4j', module: 'slf4j-simple' + } implementation 'org.projectlombok:lombok:1.18.24' annotationProcessor 'org.projectlombok:lombok:1.18.24' @@ -56,7 +58,7 @@ dependencies { exclude group: 'ch.qos.logback', module: 'logback-classic' } - implementation 'com.github.OSRSB:OSRSBPlugin:main-SNAPSHOT' + // implementation 'com.github.OSRSB:OSRSBPlugin:main-SNAPSHOT' // JitPack can't resolve implementation group: 'com.github.joonasvali.naturalmouse', name: 'naturalmouse', version: '2.0.3' implementation group: 'javassist', name: 'javassist', version: '3.12.1.GA' implementation group: 'net.sf.jopt-simple', name:'jopt-simple', version: '5.0.4' @@ -67,6 +69,9 @@ sourceSets { java { srcDirs= ["src/main/java"] } + resources { + srcDirs= ["src/main/resources"] + } } } diff --git a/launch-bot.bat b/launch-bot.bat new file mode 100644 index 00000000..a07fda57 --- /dev/null +++ b/launch-bot.bat @@ -0,0 +1,56 @@ +@echo off +REM ============================================================ +REM OsrsBot Launcher with Jagex Authentication Support +REM ============================================================ +REM +REM FIRST-TIME SETUP (one-time only): +REM 1. Open "RuneLite (configure)" from Start Menu +REM (or manually edit %LOCALAPPDATA%\RuneLite\settings.json) +REM 2. In "Client arguments", add: --insecure-write-credentials +REM 3. Open Jagex Launcher and click Play OSRS (RuneLite) +REM 4. Log in to your account, then close RuneLite +REM 5. Verify %USERPROFILE%\.runelite\credentials.properties exists +REM 6. Remove --insecure-write-credentials from client arguments +REM 7. Now you can run this script to launch the bot! +REM +REM Tokens persist until you click "End sessions" on runescape.com +REM ============================================================ + +set JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-17.0.18.8-hotspot +set JAVA="%JAVA_HOME%\bin\java.exe" + +if not exist %JAVA% ( + echo ERROR: Java 17 not found at %JAVA_HOME% + echo Please install Eclipse Adoptium JDK 17 + pause + exit /b 1 +) + +REM Load Jagex credentials as environment variables so the game client +REM can authenticate via System.getenv("JX_ACCESS_TOKEN") etc. +set CREDS=%USERPROFILE%\.runelite\credentials.properties +if exist "%CREDS%" ( + echo Loading Jagex credentials from %CREDS% + for /f "usebackq tokens=1,* delims==" %%a in ("%CREDS%") do ( + REM Skip comments (lines starting with #) + echo %%a | findstr /b "#" >nul 2>&1 + if errorlevel 1 ( + set "%%a=%%b" + echo Set env: %%a + ) + ) +) else ( + echo WARNING: No credentials.properties found! + echo. + echo You need to set up Jagex authentication first: + echo 1. Open "RuneLite (configure)" from Start Menu + echo 2. Add --insecure-write-credentials to "Client arguments" + echo 3. Launch RuneLite via Jagex Launcher and log in + echo 4. Close RuneLite, then run this script again + echo. + echo Launching without credentials - you may not be able to log in... + echo. +) + +cd /d "%~dp0" +%JAVA% -Xmx2g -XX:ReservedCodeCacheSize=512m -XX:HeapBaseMinAddress=0x10000000 -jar OSRSBot.jar --bot-runelite --developer-mode %* diff --git a/launch-bot.sh b/launch-bot.sh new file mode 100644 index 00000000..fb565fd6 --- /dev/null +++ b/launch-bot.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# ============================================================ +# OsrsBot Launcher with Jagex Authentication Support +# ============================================================ +# +# FIRST-TIME SETUP (one-time only): +# 1. Open "RuneLite (configure)" from Start Menu +# (or manually edit %LOCALAPPDATA%\RuneLite\settings.json) +# 2. In "Client arguments", add: --insecure-write-credentials +# 3. Open Jagex Launcher and click Play OSRS (RuneLite) +# 4. Log in to your account, then close RuneLite +# 5. Verify ~/.runelite/credentials.properties exists +# 6. Remove --insecure-write-credentials from client arguments +# 7. Now you can run this script! +# +# Tokens persist until you click "End sessions" on runescape.com +# ============================================================ + +export JAVA_HOME="/c/Program Files/Eclipse Adoptium/jdk-17.0.18.8-hotspot" +JAVA="$JAVA_HOME/bin/java.exe" + +if [ ! -f "$JAVA" ]; then + echo "ERROR: Java 17 not found at $JAVA_HOME" + exit 1 +fi + +# Load Jagex credentials as environment variables so the game client +# can authenticate via System.getenv("JX_ACCESS_TOKEN") etc. +CREDS="$HOME/.runelite/credentials.properties" +if [ -f "$CREDS" ]; then + echo "Loading Jagex credentials from $CREDS" + while IFS='=' read -r key value; do + # Skip comments and blank lines + case "$key" in + \#*|"") continue ;; + esac + # Trim whitespace + key=$(echo "$key" | xargs) + value=$(echo "$value" | xargs) + if [ -n "$key" ] && [ -n "$value" ]; then + export "$key=$value" + echo " Set env: $key" + fi + done < "$CREDS" +else + echo "WARNING: No credentials.properties found at $CREDS" + echo "" + echo "You need to set up Jagex authentication first:" + echo " 1. Open 'RuneLite (configure)' from Start Menu" + echo " 2. Add --insecure-write-credentials to 'Client arguments'" + echo " 3. Launch RuneLite via Jagex Launcher and log in" + echo " 4. Close RuneLite, then run this script again" + echo "" + echo "Launching without credentials - you may not be able to log in..." + echo "" +fi + +cd "$(dirname "$0")" +"$JAVA" -Xmx2g -XX:ReservedCodeCacheSize=512m -XX:HeapBaseMinAddress=0x10000000 -jar OSRSBot.jar --bot-runelite --developer-mode "$@" diff --git a/scripts/osrsbot-learning-0.1.0-SNAPSHOT.jar b/scripts/osrsbot-learning-0.1.0-SNAPSHOT.jar new file mode 100644 index 00000000..da507a12 Binary files /dev/null and b/scripts/osrsbot-learning-0.1.0-SNAPSHOT.jar differ diff --git a/src/main/java/net/runelite/client/modified/BotModule.java b/src/main/java/net/runelite/client/modified/BotModule.java index 1e5e75e7..1e1328f1 100644 --- a/src/main/java/net/runelite/client/modified/BotModule.java +++ b/src/main/java/net/runelite/client/modified/BotModule.java @@ -10,7 +10,6 @@ import com.google.inject.binder.ConstantBindingBuilder; import com.google.inject.name.Named; import com.google.inject.name.Names; -import java.applet.Applet; import java.io.File; import java.util.Map; import java.util.Properties; @@ -47,7 +46,7 @@ public class BotModule extends AbstractModule { private final OkHttpClient okHttpClient; - private final Supplier clientLoader; + private final Supplier clientLoader; private final RuntimeConfigLoader configSupplier; private final boolean developerMode; private final boolean safeMode; @@ -60,7 +59,7 @@ public class BotModule extends AbstractModule { private final boolean noupdate = false; - public BotModule(OkHttpClient okHttpClient, Supplier clientLoader, RuntimeConfigLoader configSupplier, boolean developerMode, boolean safeMode, File sessionfile, File config) { + public BotModule(OkHttpClient okHttpClient, Supplier clientLoader, RuntimeConfigLoader configSupplier, boolean developerMode, boolean safeMode, File sessionfile, File config) { this.okHttpClient = okHttpClient; this.clientLoader = clientLoader; this.configSupplier = configSupplier; @@ -142,18 +141,11 @@ else if (entry.getValue() instanceof Double) @Provides @Singleton - Applet provideApplet() + Client provideClient() { return clientLoader.get(); } - @Provides - @Singleton - Client provideClient(@Nullable Applet applet) - { - return applet instanceof Client ? (Client) applet : null; - } - @Provides @Singleton RuntimeConfig provideRuntimeConfig() diff --git a/src/main/java/net/runelite/client/modified/RuneLite.java b/src/main/java/net/runelite/client/modified/RuneLite.java index 369ef2bd..7fe41385 100644 --- a/src/main/java/net/runelite/client/modified/RuneLite.java +++ b/src/main/java/net/runelite/client/modified/RuneLite.java @@ -10,7 +10,6 @@ import com.google.inject.Inject; import com.google.inject.Injector; -import java.applet.Applet; import java.io.File; import java.io.IOException; import java.lang.management.ManagementFactory; @@ -35,7 +34,6 @@ import javax.swing.*; import joptsimple.*; -import joptsimple.util.EnumConverter; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.runelite.api.*; @@ -49,7 +47,6 @@ import net.runelite.client.game.WorldService; import net.runelite.client.plugins.PluginManager; import net.runelite.client.rs.ClientLoader; -import net.runelite.client.rs.ClientUpdateCheckMode; import net.runelite.client.ui.FontManager; import net.runelite.client.ui.overlay.WidgetOverlay; import net.runelite.rsb.botLauncher.BotLite; @@ -124,10 +121,6 @@ public class RuneLite extends net.runelite.client.RuneLite { @Nullable public Client client; - @Inject - @Nullable - public Applet applet; - @Inject @Nullable private RuntimeConfig runtimeConfig; @@ -175,23 +168,11 @@ public static ArgumentAcceptingOptionSpec[] handleParsing(OptionParser parser .withValuesConvertedBy(new ConfigFileConverter()) .defaultsTo(DEFAULT_CONFIG_FILE); - final ArgumentAcceptingOptionSpec updateMode = parser - .accepts("rs", "Select client type") - .withRequiredArg() - .ofType(ClientUpdateCheckMode.class) - .defaultsTo(ClientUpdateCheckMode.AUTO) - .withValuesConvertedBy(new EnumConverter<>(ClientUpdateCheckMode.class) { - @Override - public ClientUpdateCheckMode convert(String v) { - return super.convert(v.toUpperCase()); - } - }); - final ArgumentAcceptingOptionSpec proxyInfo = parser .accepts("proxy", "Designates a proxy ip address to be used to make the bot server connections") .withRequiredArg().ofType(String.class); - return (ArgumentAcceptingOptionSpec[]) new ArgumentAcceptingOptionSpec[]{sessionfile, configfile, updateMode, proxyInfo}; + return (ArgumentAcceptingOptionSpec[]) new ArgumentAcceptingOptionSpec[]{sessionfile, configfile, proxyInfo}; } /** @@ -260,7 +241,6 @@ public static void initializeClient(ArgumentAcceptingOptionSpec[] optionSpecs { final RuntimeConfigLoader runtimeConfigLoader = new RuntimeConfigLoader(okHttpClient); final ClientLoader clientLoader = new ClientLoader(okHttpClient, - options.valueOf(optionSpecs[Options.UPDATE_MODE.getIndex()].ofType(ClientUpdateCheckMode.class)), runtimeConfigLoader, (String) options.valueOf("jav_config")); @@ -277,7 +257,7 @@ public static void initializeClient(ArgumentAcceptingOptionSpec[] optionSpecs injector = Guice.createInjector(new BotModule( okHttpClient, - clientLoader, + clientLoader::get, runtimeConfigLoader, options.has("developer-mode"), false, @@ -319,25 +299,20 @@ public void bareStart() throws Exception { injector.injectMembers(client); } - // Start the applet - if (applet != null) + // Initialize the client via GameEngine API + if (client != null) { - // Client size must be set prior to init - applet.setSize(Constants.GAME_FIXED_SIZE); - System.setProperty("jagex.disableBouncyCastle", "true"); // Change user.home so the client places jagexcache in the .runelite directory String oldHome = System.setProperty("user.home", RUNELITE_DIR.getAbsolutePath()); try { - applet.init(); + client.initialize(); } finally { System.setProperty("user.home", oldHome); } - - applet.start(); } // Load user configuration @@ -367,6 +342,36 @@ public void bareStart() throws Exception { } } + /** + * Overrides parent RuneLite.start() to fix sidebar initialization order. + * The parent's start() loads and starts plugins BEFORE calling clientUI.init(), + * which causes NPEs when plugins try to add navigation buttons to a null sidebar. + * This override ensures the sidebar is created first via bareStart(), then loads + * and starts plugins, then shows the UI. + */ + @Override + public void start() throws Exception { + // Step 1: Client init, config loading, sidebar creation (clientUI.init()) + bareStart(); + + // Step 2: Load plugins — sidebar now exists for addNavigation calls + pluginManager.loadCorePlugins(); + pluginManager.loadSideLoadPlugins(); + externalPluginManager.loadExternalPlugins(); + pluginManager.loadDefaultPluginConfiguration(null); + + // Step 3: Start plugins — startUp() calls addNavigation safely now + pluginManager.startPlugins(); + + // Step 4: Discord, show UI, unblock client + discordService.init(); + eventBus.register(discordService); + clientUI.show(); + if (client != null) { + client.unblockStartup(); + } + } + /** * RuneLite method * Converts config files paths to whatever directory needed @@ -527,7 +532,7 @@ private void setupSystemProps() * The values assigned are their positions within the relating ArgumentAcceptingOptionSpec array */ enum Options { - SESSION_FILE(0),CONFIG_FILE(1), UPDATE_MODE(2), PROXY_INFO(3); + SESSION_FILE(0), CONFIG_FILE(1), PROXY_INFO(2); private int index; diff --git a/src/main/java/net/runelite/client/plugins/bot/AccountPanel.java b/src/main/java/net/runelite/client/plugins/bot/AccountPanel.java new file mode 100644 index 00000000..8cf30511 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/bot/AccountPanel.java @@ -0,0 +1,134 @@ +package net.runelite.client.plugins.bot; + +import net.runelite.client.ui.PluginPanel; +import net.runelite.rsb.botLauncher.BotLite; +import net.runelite.rsb.botLauncher.BotLiteInterface; +import net.runelite.rsb.internal.ScriptHandler; +import net.runelite.rsb.internal.listener.ScriptListener; +import net.runelite.rsb.plugin.AccountManager; +import net.runelite.rsb.script.Script; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; + +/** + * A class that acts as effectively a single button to launch the account manager interface + */ +public class AccountPanel extends PluginPanel implements ScriptListener { + + private JScrollPane scrollPane1; + private JTable table1; + private JButton buttonAccounts; + private JButton buttonScripts; + private BotLite bot; + + /** + * Creates an account panel bound to a singleton of a bot + * TODO: Change this to not be hard bound, but rather iterable through a list of active clients + * @param bot the bot singleton to associate with this panel + */ + public AccountPanel(BotLite bot) { + initComponents(); + this.bot = bot; + bot.getScriptHandler().init(); + } + + /** + * Assigns the action for the accounts button action + * @param e the action event to assign + */ + private void buttonAccountActionPerformed(ActionEvent e) { + AccountManager.getInstance().showGUI(); + } + + /** + * Initializes the components for the panel + */ + private void initComponents() { + scrollPane1 = new JScrollPane(); + table1 = new JTable(); + buttonAccounts = new JButton(); + buttonScripts = new JButton(); + + + buttonAccounts.setText("View Accounts"); + buttonAccounts.addActionListener(e -> buttonAccountActionPerformed(e)); + add(buttonAccounts); + buttonAccounts.setBounds(new Rectangle(new Point(15, 375), buttonAccounts.getPreferredSize())); + + + { + // compute preferred size + Dimension preferredSize = new Dimension(); + for(int i = 0; i < getComponentCount(); i++) { + Rectangle bounds = getComponent(i).getBounds(); + preferredSize.width = Math.max(bounds.x + bounds.width, preferredSize.width); + preferredSize.height = Math.max(bounds.y + bounds.height, preferredSize.height); + } + Insets insets = getInsets(); + preferredSize.width += insets.right; + preferredSize.height += insets.bottom; + setMinimumSize(preferredSize); + setPreferredSize(preferredSize); + } + + } + + + /** + * Handles any task necessary if a script has been started + * + * @param handler the script handler + * @param script the script to start + */ + public void scriptStarted(final ScriptHandler handler, Script script) { + EventQueue.invokeLater(new Runnable() { + public void run() { + BotLiteInterface bot = handler.getBot(); + } + }); + } + + + /** + * Handles any task necessary if a script has been stopped + * + * @param handler the script handler + * @param script the script to stop + */ + public void scriptStopped(ScriptHandler handler, Script script) { + + } + + /** + * Handles any task necessary on a script being resumed + * + * @param handler the script handler + * @param script the script to resume + */ + public void scriptResumed(ScriptHandler handler, Script script) { + + } + + /** + * Handles any task necessary on a script being paused + * + * @param handler the script handler + * @param script the script to pause + */ + public void scriptPaused(ScriptHandler handler, Script script) { + + } + + + /** + * Handles any task necessary if the input has changed + * + * @param bot the bot instance to check + * @param mask the mask to check for + */ + public void inputChanged(BotLite bot, int mask) { + + } +} diff --git a/src/main/java/net/runelite/client/plugins/bot/BotConfig.java b/src/main/java/net/runelite/client/plugins/bot/BotConfig.java new file mode 100644 index 00000000..6a97d24b --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/bot/BotConfig.java @@ -0,0 +1,152 @@ +package net.runelite.client.plugins.bot; + +import ch.qos.logback.classic.Level; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("bot") +public interface BotConfig extends Config { + @ConfigItem( + keyName = "bot", + name = "OSRSBot", + description = "Fuck Adam", + position = 0 + ) + default boolean bot() { + return true; + } + + @ConfigItem( + keyName = "debugLogLevel", + name = "Debug Log Level", + description = "Set the log level of bot", + hidden = true, + position = 1 + ) + default String debugLogLevel() { + return "INFO"; + } + + @ConfigItem( + keyName = "debugDrawMouse", + name = "Draw Mouse", + description = "Draw the mouse on script start", + hidden = true, + position = 2 + ) + default boolean debugDrawMouse() { + return false; + } + + @ConfigItem( + keyName = "debugDrawMouseTrail", + name = "Draw Mouse Trail", + description = "Draw the mouse trail on script start", + hidden = true, + position = 3 + ) + default boolean debugDrawMouseTrail() { + return false; + } + + @ConfigItem( + keyName = "debugEnableMouse", + name = "Enable Mouse", + description = "Enable the mouse on script start", + hidden = true, + position = 4 + ) + default boolean debugEnableMouse() { + return true; + } + + @ConfigItem( + keyName = "debugDrawBoundaries", + name = "Draw Boundaries", + description = "Draw boundaries", + hidden = true, + position = 5 + ) + default boolean debugDrawBoundaries() { + return false; + } + + @ConfigItem( + keyName = "debugDrawGround", + name = "Draw Ground", + description = "Draw ground", + hidden = true, + position = 6 + ) + default boolean debugDrawGround() { + return false; + } + + @ConfigItem( + keyName = "debugDrawInventory", + name = "Draw Inventory", + description = "Draw inventory", + hidden = true, + position = 7 + ) + default boolean debugDrawInventory() { + return false; + } + + @ConfigItem( + keyName = "debugDrawNPCs", + name = "Draw NPCs", + description = "Draw NPCs", + hidden = true, + position = 8 + ) + default boolean debugDrawNPCs() { + return false; + } + + @ConfigItem( + keyName = "debugDrawObjects", + name = "Draw Objects", + description = "Draw objects", + hidden = true, + position = 9 + ) + default boolean debugDrawObjects() { + return false; + } + + @ConfigItem( + keyName = "debugDrawPlayers", + name = "Draw Players", + description = "Draw players", + hidden = true, + position = 10 + ) + default boolean debugDrawPlayers() { + return false; + } + + @ConfigItem( + keyName = "debugDrawSettings", + name = "Draw Settings", + description = "Draw settings", + hidden = true, + position = 11 + ) + default boolean debugDrawSettings() { + return false; + } + + @ConfigItem( + keyName = "debugDrawWeb", + name = "Draw Web", + description = "Draw web", + hidden = true, + position = 12 + ) + default boolean debugDrawWeb() { + return false; + } + +} diff --git a/src/main/java/net/runelite/client/plugins/bot/BotPanel.java b/src/main/java/net/runelite/client/plugins/bot/BotPanel.java new file mode 100644 index 00000000..77b5688e --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/bot/BotPanel.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2018, SomeoneWithAnInternetConnection + * Copyright (c) 2018, Psikoi + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.runelite.client.plugins.bot; + +import net.runelite.client.plugins.bot.base.Accordion; +import net.runelite.client.plugins.bot.base.BotViewPanel; +import net.runelite.client.plugins.bot.base.DebugPanel; +import net.runelite.client.plugins.bot.base.GeneralPanel; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.materialtabs.MaterialTab; +import net.runelite.client.ui.components.materialtabs.MaterialTabGroup; + +import javax.inject.Inject; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; + +public class BotPanel extends PluginPanel +{ + private final JPanel display = new JPanel(); + private final MaterialTabGroup tabGroup = new MaterialTabGroup(display); + + + @Inject + private BotPanel() + { + super(false); + setLayout(new BorderLayout()); + setBackground(ColorScheme.DARK_GRAY_COLOR); + } + + /** + * Associates the bot plugin panel with the script panel. Since the Bot Plugin has access to the injector the instance + * of RuneLite must be passed forward through the scriptPanel constructor for script selection to work. + * + * @param accountPanel The account panel to associate with the bot plugin panel. + * @param scriptPanel The script panel to associate with the bot plugin panel. + */ + public void associateBot(AccountPanel accountPanel, ScriptPanel scriptPanel, DebugPanel debugPanel) { + + + Accordion accordion = new Accordion(); + + JPanel generalPanel = GeneralPanel.getInstance(); + JPanel botViewPanel = new BotViewPanel(); + + accordion.addBar("General", generalPanel); + accordion.addBar("Debug", debugPanel); + accordion.addBar("Bot View", botViewPanel); + + MaterialTab baseTab = new MaterialTab("Settings", tabGroup, accordion); + MaterialTab accountTab = new MaterialTab("Accounts", tabGroup, accountPanel); + JScrollPane botPanelScrollPane = new JScrollPane(scriptPanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + MaterialTab scriptTab = new MaterialTab("Scripts", tabGroup, botPanelScrollPane); + + + tabGroup.setBorder(new EmptyBorder(5, 0, 0, 0)); + tabGroup.addTab(baseTab); + tabGroup.addTab(accountTab); + tabGroup.addTab(scriptTab); + tabGroup.select(baseTab); + + add(tabGroup, BorderLayout.NORTH); + add(display, BorderLayout.CENTER); + } + +} \ No newline at end of file diff --git a/src/main/java/net/runelite/client/plugins/bot/BotPlugin.java b/src/main/java/net/runelite/client/plugins/bot/BotPlugin.java new file mode 100644 index 00000000..fe21400d --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/bot/BotPlugin.java @@ -0,0 +1,145 @@ +package net.runelite.client.plugins.bot; + +import ch.qos.logback.classic.Level; +import com.google.inject.Provides; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.bot.base.DebugPanel; +import net.runelite.client.ui.ClientToolbar; +import net.runelite.client.ui.NavigationButton; +import net.runelite.rsb.botLauncher.BotLite; + +import javax.imageio.ImageIO; +import javax.inject.Inject; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +@PluginDescriptor( + name = "Bot panel", + description = "Bot panel" +) +@Slf4j +public class BotPlugin extends Plugin { + + @Inject + private BotConfig config; + + @Inject + private ClientToolbar clientToolbar; + + private NavigationButton navButton; + + private static ScriptPanel scriptPanel; + + private static AccountPanel accountPanel; + + private static DebugPanel debugPanel; + + public BotPlugin() { + } + + @Provides + BotConfig provideConfig(ConfigManager configManager) { + return (BotConfig)configManager.getConfig(BotConfig.class); + } + @Subscribe + public void onConfigChanged(ConfigChanged configChanged) { + if (configChanged.getGroup().equals("bot")) { + switch(configChanged.getKey()) { + case "debugLogLevel" -> + debugPanel.setLogLevel(Level.valueOf(configChanged.getNewValue())); + case "debugDrawMouse" -> + debugPanel.setDrawMouse(configChanged.getNewValue() == "true"); + case "debugDrawMouseTrail" -> + debugPanel.setDrawMouseTrail(configChanged.getNewValue() == "true"); + case "debugEnableMouse" -> + debugPanel.setEnableMouse(configChanged.getNewValue() == "true"); + case "debugDrawBoundaries" -> + debugPanel.setDrawBoundaries(configChanged.getNewValue() == "true"); + case "debugDrawGround" -> + debugPanel.setDrawGround(configChanged.getNewValue() == "true"); + case "debugDrawInventory" -> + debugPanel.setDrawInventory(configChanged.getNewValue() == "true"); + case "debugDrawNpcs" -> + debugPanel.setDrawNPCs(configChanged.getNewValue() == "true"); + case "debugDrawObjects" -> + debugPanel.setDrawObjects(configChanged.getNewValue() == "true"); + case "debugDrawPlayers" -> + debugPanel.setDrawPlayers(configChanged.getNewValue() == "true"); + case "debugDrawWeb" -> + debugPanel.setDrawWeb(configChanged.getNewValue() == "true"); + } + } + } + + @Override + protected void startUp() throws Exception + { + BotPanel panel = injector.getInstance(BotPanel.class); + BotLite bot = injector.getInstance(BotLite.class); + + accountPanel = new AccountPanel(bot); + scriptPanel = new ScriptPanel(bot); + debugPanel = new DebugPanel(bot); + + panel.associateBot(accountPanel, scriptPanel, debugPanel); + + BufferedImage icon = imageToBufferedImage(BotPlugin.class.getResourceAsStream("rsb.png")); + + navButton = NavigationButton.builder() + .tooltip("Bot Interface") + .icon(icon) + .priority(10) + .panel(panel) + .build(); + clientToolbar = injector.getInstance(ClientToolbar.class); + clientToolbar.addNavigation(navButton); + } + + @Override + protected void shutDown() + { + clientToolbar.removeNavigation(navButton); + } + + public static BufferedImage imageToBufferedImage(InputStream is) throws IOException { + Image im = ImageIO.read(is); + BufferedImage bi = new BufferedImage + (im.getWidth(null),im.getHeight(null),BufferedImage.TYPE_INT_RGB); + Graphics bg = bi.getGraphics(); + bg.drawImage(im, 0, 0, null); + bg.dispose(); + return bi; + } + + public static ScriptPanel getScriptPanel() { + return scriptPanel; + } + + public static void setScriptPanel(ScriptPanel scriptPanel) { + BotPlugin.scriptPanel = scriptPanel; + } + + public static AccountPanel getAccountPanel() { + return accountPanel; + } + + public void setAccountPanel(AccountPanel accountPanel) { + BotPlugin.accountPanel = accountPanel; + } + + public static DebugPanel getDebugPanel() { + return debugPanel; + } + + public void setDebugPanel(DebugPanel debugPanel) { + BotPlugin.debugPanel = debugPanel; + } +} diff --git a/src/main/java/net/runelite/client/plugins/bot/ScriptPanel.java b/src/main/java/net/runelite/client/plugins/bot/ScriptPanel.java new file mode 100644 index 00000000..975bb15e --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/bot/ScriptPanel.java @@ -0,0 +1,245 @@ +package net.runelite.client.plugins.bot; + +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.materialtabs.MaterialTab; +import net.runelite.client.ui.components.materialtabs.MaterialTabGroup; +import net.runelite.client.util.ImageUtil; +import net.runelite.rsb.botLauncher.BotLite; +import net.runelite.rsb.internal.globval.GlobalConfiguration; +import net.runelite.rsb.plugin.ScriptSelector; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.io.IOException; + +public class ScriptPanel extends PluginPanel { + private BotLite bot; + private JScrollPane scrollPane1; + private JScrollPane scriptsSelectionScrollPane; + private JTable scriptsTable; + private JComboBox comboBoxAccounts; + private JButton buttonStart; + private JButton buttonPause; + private JButton buttonStop; + private MaterialTab buttonScriptsFolder; + private JButton buttonForums; + private ScriptSelector scriptSelector; + private MaterialTabGroup scriptPanelToolbar; + + public ScriptPanel(BotLite bot) { + this.bot = bot; + scriptSelector = new ScriptSelector(bot); + initComponents(); + } + + /** + * Opens the scripts folder in the default file explorer + * + * @param e ActionEvent + */ + private void openForumsPerformed(ActionEvent e) { + String forumsURL = GlobalConfiguration.Paths.URLs.FORUMS; + try { + switch (GlobalConfiguration.getCurrentOperatingSystem()) { + case WINDOWS: + Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + forumsURL); + break; + case LINUX: + Runtime.getRuntime().exec("xdg-open " + forumsURL); + break; + case MAC: + Runtime.getRuntime().exec("open " + forumsURL); + break; + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + /** + * Opens the scripts folder in the default file explorer + * + * @param e ActionEvent + */ + private void openScriptsFolderPerformed(ActionEvent e) { + String folderPath = GlobalConfiguration.Paths.getScriptsPrecompiledDirectory(); + try { + switch (GlobalConfiguration.getCurrentOperatingSystem()) { + case WINDOWS: + Runtime.getRuntime().exec("explorer.exe " + folderPath); + break; + case LINUX: + Runtime.getRuntime().exec("xdg-open " + folderPath); + break; + case MAC: + Runtime.getRuntime().exec("open " + folderPath); + break; + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + private void initComponents() { + scriptsSelectionScrollPane = new JScrollPane(); + scriptSelector.accounts = scriptSelector.getAccounts(); + //Make a search area + scriptSelector.getSearch(); + scriptSelector.load(); + buttonForums = new JButton(); + + scriptPanelToolbar = new MaterialTabGroup(); + scriptPanelToolbar.setLayout(new GridLayout(1, 5, 5, 5)); + + //======== this ======== + setBorder (new javax. swing. border. CompoundBorder( new javax .swing .border .TitledBorder (new javax. swing. border. EmptyBorder( 0 + , 0, 0, 0) , "", javax. swing. border. TitledBorder. CENTER, javax. swing. border. TitledBorder. BOTTOM + , new Font ("D\u0069alog" , Font .BOLD ,12 ), Color. red) , + getBorder( )) ); addPropertyChangeListener (new java. beans. PropertyChangeListener( ){ @Override public void propertyChange (java .beans .PropertyChangeEvent e + ) {if ("\u0062order" .equals (e .getPropertyName () )) throw new RuntimeException( ); }} ); + + //======== scripts scroll pane ======== + scriptsSelectionScrollPane.setViewportView(scriptSelector.table); + + //---- buttonStart ---- + //scriptSelector.buttonStart.setText("Start"); + //scriptSelector.buttonStart.addActionListener(scriptSelector::buttonStartActionPerformed); + final BufferedImage startIcon = ImageUtil.loadImageResource(getClass(), "start.png"); + scriptSelector.buttonStart = new MaterialTab(new ImageIcon(startIcon.getScaledInstance(24, 24, 5)), scriptPanelToolbar, null); + scriptSelector.buttonStart.setToolTipText("Start selected script"); + scriptSelector.buttonStart.setSize(new Dimension(28, 28)); + scriptSelector.buttonStart.setMinimumSize(new Dimension(0, 28)); + scriptSelector.buttonStart.setEnabled(false); + scriptSelector.buttonStart.setOpaque(true); + scriptSelector.buttonStart.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + scriptSelector.buttonStartActionPerformed(null); + } + }); + scriptPanelToolbar.addTab(scriptSelector.buttonStart); + + //---- buttonPause ---- + //scriptSelector.buttonPause.setText("Pause"); + //scriptSelector.buttonPause.addActionListener(scriptSelector::buttonPauseActionPerformed); + final BufferedImage pauseIcon = ImageUtil.loadImageResource(getClass(), "pause.png"); + scriptSelector.buttonPause = new MaterialTab(new ImageIcon(pauseIcon.getScaledInstance(20, 20, 5)), scriptPanelToolbar, null); + scriptSelector.buttonPause.setToolTipText("Pause the active script"); + scriptSelector.buttonPause.setSize(new Dimension(28, 28)); + scriptSelector.buttonPause.setMinimumSize(new Dimension(0, 0)); + scriptSelector.buttonPause.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + scriptSelector.buttonPauseActionPerformed(null); + } + }); + scriptPanelToolbar.addTab(scriptSelector.buttonPause); + + //---- buttonStop ---- + //scriptSelector.buttonStop.setText("Stop"); + //scriptSelector.buttonStop.addActionListener(scriptSelector::buttonStopActionPerformed); + final BufferedImage stopIcon = ImageUtil.loadImageResource(getClass(), "stop.png"); + scriptSelector.buttonStop = new MaterialTab(new ImageIcon(stopIcon.getScaledInstance(20, 20, 5)), scriptPanelToolbar, null); + scriptSelector.buttonStop.setToolTipText("Stop running the active script"); + scriptSelector.buttonStop.setSize(new Dimension(28, 28)); + scriptSelector.buttonStop.setMinimumSize(new Dimension(0, 28)); + scriptSelector.buttonStop.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + scriptSelector.buttonStopActionPerformed(null); + } + }); + scriptPanelToolbar.addTab(scriptSelector.buttonStop); + + //---- buttonReload ---- + final BufferedImage iconImage = ImageUtil.loadImageResource(getClass(), "reload.png"); + scriptSelector.buttonReload = new MaterialTab(new ImageIcon(iconImage.getScaledInstance(20, 20, 5)), scriptPanelToolbar, null); + scriptSelector.buttonReload.setToolTipText("Reload Plugins"); + scriptSelector.buttonReload.setSize(new Dimension(28, 28)); + scriptSelector.buttonReload.setMinimumSize(new Dimension(0, 28)); + scriptSelector.buttonReload.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + scriptSelector.buttonReloadActionPerformed(); + } + }); + scriptPanelToolbar.addTab(scriptSelector.buttonReload); + + //---- buttonScriptsFolder ---- + final BufferedImage folder = ImageUtil.loadImageResource(getClass(), "open-folder.png"); + buttonScriptsFolder = new MaterialTab(new ImageIcon(folder.getScaledInstance(20, 20, 5)), scriptPanelToolbar, null); + buttonScriptsFolder.setToolTipText("Open scripts folder"); + buttonScriptsFolder.setOpaque(true); + buttonScriptsFolder.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + openScriptsFolderPerformed(null); + } + }); + scriptPanelToolbar.addTab(buttonScriptsFolder); + + //---- buttonForums ---- + buttonForums.setText("Forums"); + buttonForums.addActionListener(e -> openForumsPerformed(e)); + assignLayouts(); + } + + /** + * Assigns the layouts for the script panel + */ + private void assignLayouts() { + GroupLayout layout = new GroupLayout(this); + setLayout(layout); + + layout.setHorizontalGroup( + layout.createParallelGroup() + .addComponent(scriptPanelToolbar, 0, 240, Short.MAX_VALUE) + .addGap(10, 10, 10) + .addComponent(scriptsSelectionScrollPane, GroupLayout.DEFAULT_SIZE, 240, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup() + .addGroup(layout.createSequentialGroup() + .addGap(47, 47, 47) + .addComponent(scriptSelector.accounts, GroupLayout.PREFERRED_SIZE, 157, GroupLayout.PREFERRED_SIZE)) + + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGap(18, 18, 18) + .addComponent(buttonForums, GroupLayout.PREFERRED_SIZE, 84, GroupLayout.PREFERRED_SIZE)) + .addGap(10, 10, 10)) + .addContainerGap(30, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup() + .addGroup(layout.createSequentialGroup() + .addComponent(scriptPanelToolbar, 28, 40, 40) + .addComponent(scriptsSelectionScrollPane, GroupLayout.PREFERRED_SIZE, 240, GroupLayout.PREFERRED_SIZE) + .addGap(10, 10, 10) + .addComponent(scriptSelector.accounts, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addGap(0, 10, Short.MAX_VALUE) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(buttonForums, GroupLayout.PREFERRED_SIZE, 33, GroupLayout.PREFERRED_SIZE)) + .addContainerGap(10, Short.MAX_VALUE)) + + + ); + } + + /** + * Redefines the list of accounts in the dropdown list with an updated set of values + * by reassigning the model + * TODO: Add this to an event listener + */ + public void updateAccountList() { + scriptSelector.accounts.setModel(scriptSelector.getAccounts().getModel()); + } +} diff --git a/src/main/java/net/runelite/client/plugins/bot/base/Accordion.java b/src/main/java/net/runelite/client/plugins/bot/base/Accordion.java new file mode 100644 index 00000000..4cd4829f --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/bot/base/Accordion.java @@ -0,0 +1,326 @@ +package net.runelite.client.plugins.bot.base; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A JOutlookBar provides a component that is similar to a JTabbedPane, but instead of maintaining + * tabs, it uses Outlook-style bars to control the visible component + */ +public class Accordion extends JPanel implements ActionListener +{ + /** + * The top panel: contains the buttons displayed on the top of the JOutlookBar + */ + private JPanel topPanel = new JPanel( new GridLayout( 1, 1 ) ); + + /** + * The bottom panel: contains the buttons displayed on the bottom of the JOutlookBar + */ + private JPanel bottomPanel = new JPanel( new GridLayout( 1, 1 ) ); + + /** + * A LinkedHashMap of bars: we use a linked hash map to preserve the order of the bars + */ + private Map bars = new LinkedHashMap(); + + /** + * The currently visible bar (zero-based index) + */ + private int visibleBar = 0; + + /** + * A place-holder for the currently visible component + */ + private JComponent visibleComponent = null; + + /** + * Creates a new JOutlookBar; after which you should make repeated calls to + * addBar() for each bar + */ + public Accordion() + { + this.setLayout( new BorderLayout() ); + this.add( topPanel, BorderLayout.NORTH ); + this.add( bottomPanel, BorderLayout.SOUTH ); + } + + /** + * Adds the specified component to the JOutlookBar and sets the bar's name + * + * @param name The name of the outlook bar + * @param component The component to add to the bar + */ + public void addBar( String name, JComponent component ) + { + BarInfo barInfo = new BarInfo( name, component ); + barInfo.getButton().addActionListener( this ); + this.bars.put( name, barInfo ); + render(); + } + + /** + * Adds the specified component to the JOutlookBar and sets the bar's name + * + * @param name The name of the outlook bar + * @param icon An icon to display in the outlook bar + * @param component The component to add to the bar + */ + public void addBar( String name, Icon icon, JComponent component ) + { + BarInfo barInfo = new BarInfo( name, icon, component ); + barInfo.getButton().addActionListener( this ); + this.bars.put( name, barInfo ); + render(); + } + + /** + * Removes the specified bar from the JOutlookBar + * + * @param name The name of the bar to remove + */ + public void removeBar( String name ) + { + this.bars.remove( name ); + render(); + } + + /** + * Returns the index of the currently visible bar (zero-based) + * + * @return The index of the currently visible bar + */ + public int getVisibleBar() + { + return this.visibleBar; + } + + /** + * Programmatically sets the currently visible bar; the visible bar + * index must be in the range of 0 to size() - 1 + * + * @param visibleBar The zero-based index of the component to make visible + */ + public void setVisibleBar( int visibleBar ) + { + if( visibleBar > 0 && + visibleBar < this.bars.size() - 1 ) + { + this.visibleBar = visibleBar; + render(); + } + } + + /** + * Causes the outlook bar component to rebuild itself; this means that + * it rebuilds the top and bottom panels of bars as well as making the + * currently selected bar's panel visible + */ + public void render() + { + // Compute how many bars we are going to have where + int totalBars = this.bars.size(); + int topBars = this.visibleBar + 1; + int bottomBars = totalBars - topBars; + + + // Get an iterator to walk through out bars with + Iterator itr = this.bars.keySet().iterator(); + + + // Render the top bars: remove all components, reset the GridLayout to + // hold to correct number of bars, add the bars, and "validate" it to + // cause it to re-layout its components + this.topPanel.removeAll(); + GridLayout topLayout = ( GridLayout )this.topPanel.getLayout(); + topLayout.setRows( topBars ); + BarInfo barInfo = null; + for( int i=0; i { + paint(getGraphics()); + }); + this.add(reloadButton); + } + + // TODO: Replace; temporary preview. + public void paint(Graphics g) { + BotLiteInterface[] bots = Application.getBots(); + int width = getWidth(), height = getHeight(); + g.setColor(Color.white); + g.fillRect(0, 0, width, height); + int len = Math.min(bots.length, 6); + if (len == 1) { + draw(g, 0, 0, 0, width, height); + } else if (len == 2) { + draw(g, 0, 0, 0, width, height / 2); + draw(g, 1, 0, height / 2, width, height / 2); + } else if (len == 3) { + draw(g, 0, 0, 0, width / 2, height / 2); + draw(g, 1, width / 2, 0, width / 2, height / 2); + draw(g, 2, 0, height / 2, width, height / 2); + } else if (len == 4) { + draw(g, 0, 0, 0, width / 2, height / 2); + draw(g, 1, width / 2, 0, width / 2, height / 2); + draw(g, 2, 0, height / 2, width / 2, height / 2); + draw(g, 3, width / 2, height / 2, width / 2, height / 2); + } else if (len == 5) { + draw(g, 0, 0, 0, width / 3, height / 2); + draw(g, 1, width / 3, 0, width / 3, height / 2); + draw(g, 2, (width * 2) / 3, 0, width / 3, height / 2); + draw(g, 3, 0, height / 2, width / 2, height / 2); + draw(g, 4, width / 2, height / 2, width / 2, height / 2); + } else if (len == 6) { + draw(g, 0, 0, 0, width / 3, height / 2); + draw(g, 1, width / 3, 0, width / 3, height / 2); + draw(g, 2, (width * 2) / 3, 0, width / 3, height / 2); + draw(g, 3, 0, height / 2, width / 3, height / 2); + draw(g, 4, width / 3, height / 2, width / 3, height / 2); + draw(g, 5, (width * 2) / 3, height / 2, width / 3, height / 2); + } else { + return; + } + final Font FONT = new Font("Helvetica", 1, 13); + FontMetrics metrics = g.getFontMetrics(FONT); + g.setColor(new Color(0, 0, 0, 170)); + g.fillRect(0, height - 30, width, 30); + g.setColor(Color.white); + g.drawString("Currently running " + (bots.length == 1 ? "1 bot." : bots.length + " bots."), 5, height + metrics.getDescent() - 14); + } + + public void draw(Graphics g, int idx, int x, int y, int width, int height) { + BotLiteInterface[] bots = Application.getBots(); + BufferedImage img = ((BotLite) bots[idx]).getBackBuffer(); + if (img != null && img.getWidth() > 0) { + int w_img = img.getWidth(), h_img = img.getHeight(); + float img_ratio = (float) w_img / (float) h_img; + float bound_ratio = (float) width / (float) height; + int w, h; + if (img_ratio < bound_ratio) { + h = height; + w = (int) (((float) w_img / (float) h_img) * h); + } else { + w = width; + h = (int) (((float) h_img / (float) w_img) * w); + } + g.drawImage(img.getScaledInstance(w, h, Image.SCALE_SMOOTH), x + width / 2 - w / 2, y + height / 2 - h / 2, null); + g.setColor(Color.gray); + g.drawRect(x, y, width - 1, height - 1); + } + } +} diff --git a/src/main/java/net/runelite/client/plugins/bot/base/DebugPanel.java b/src/main/java/net/runelite/client/plugins/bot/base/DebugPanel.java new file mode 100644 index 00000000..98ba0ef3 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/bot/base/DebugPanel.java @@ -0,0 +1,149 @@ +package net.runelite.client.plugins.bot.base; + +import ch.qos.logback.classic.Level; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.plugins.bot.BotConfig; +import net.runelite.rsb.botLauncher.BotLite; + +import javax.swing.*; +import java.awt.*; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +public class DebugPanel extends JPanel { + BotLite bot; + String[] logLevels = {"DEBUG", "INFO", "WARN", "ERROR", "OFF"}; + + JComboBox logLevel = new JComboBox<>(logLevels); + JCheckBox drawMouse = new JCheckBox("Draw Mouse"); + JCheckBox drawMouseTrail = new JCheckBox("Draw Mouse Trail"); + JCheckBox enableMouse = new JCheckBox("Enable Mouse"); + JCheckBox drawBoundaries = new JCheckBox("Draw Boundaries"); + JCheckBox drawGround = new JCheckBox("Draw Ground"); + JCheckBox drawInventory = new JCheckBox("Draw Inventory"); + JCheckBox drawNPCs = new JCheckBox("Draw NPCs"); + JCheckBox drawObjects = new JCheckBox("Draw Objects"); + JCheckBox drawPlayers = new JCheckBox("Draw Players"); + JCheckBox drawSettings = new JCheckBox("Draw Settings"); + JCheckBox drawWeb = new JCheckBox("Draw Web"); + + public DebugPanel(BotLite bot) { + super(); + this.bot = bot; + init(); + } + + private void init() { + logLevel.addActionListener(e -> bot.configManager.setConfiguration("bot", "debugLogLevel", Level.valueOf(logLevel.getSelectedItem().toString()))); + drawMouse.addActionListener(e -> bot.configManager.setConfiguration("bot", "debugDrawMouse", drawMouse.isSelected())); + drawMouseTrail.addActionListener(e -> bot.configManager.setConfiguration("bot", "debugDrawMouseTrail", drawMouseTrail.isSelected())); + enableMouse.addActionListener(e -> bot.configManager.setConfiguration("bot", "debugEnableMouse", enableMouse.isSelected())); + drawBoundaries.addActionListener(e -> bot.configManager.setConfiguration("bot", "debugDrawBoundaries", drawBoundaries.isSelected())); + drawGround.addActionListener(e -> bot.configManager.setConfiguration("bot", "debugDrawGround", drawGround.isSelected())); + drawInventory.addActionListener(e -> bot.configManager.setConfiguration("bot", "debugDrawInventory", drawInventory.isSelected())); + drawNPCs.addActionListener(e -> bot.configManager.setConfiguration("bot", "debugDrawNPCs", drawNPCs.isSelected())); + drawObjects.addActionListener(e -> bot.configManager.setConfiguration("bot", "debugDrawObjects", drawObjects.isSelected())); + drawPlayers.addActionListener(e -> bot.configManager.setConfiguration("bot", "debugDrawPlayers", drawPlayers.isSelected())); + drawSettings.addActionListener(e -> bot.configManager.setConfiguration("bot", "debugDrawSettings", drawSettings.isSelected())); + drawWeb.addActionListener(e -> bot.configManager.setConfiguration("bot", "debugDrawWeb", drawWeb.isSelected())); + + BotConfig config = bot.configManager.getConfig(BotConfig.class); + if (config != null) { + logLevel.setSelectedItem(config.debugLogLevel()); + setLogLevel(Level.valueOf(config.debugLogLevel())); + drawMouse.setSelected(config.debugDrawMouse()); + setDrawMouse(config.debugDrawMouse()); + drawMouseTrail.setSelected(config.debugDrawMouseTrail()); + setDrawMouseTrail(config.debugDrawMouseTrail()); + enableMouse.setSelected(config.debugEnableMouse()); + setEnableMouse(config.debugEnableMouse()); + drawBoundaries.setSelected(config.debugDrawBoundaries()); + setDrawBoundaries(config.debugDrawBoundaries()); + drawGround.setSelected(config.debugDrawGround()); + setDrawGround(config.debugDrawGround()); + drawInventory.setSelected(config.debugDrawInventory()); + setDrawInventory(config.debugDrawInventory()); + drawNPCs.setSelected(config.debugDrawNPCs()); + setDrawNPCs(config.debugDrawNPCs()); + drawObjects.setSelected(config.debugDrawObjects()); + setDrawObjects(config.debugDrawObjects()); + drawPlayers.setSelected(config.debugDrawPlayers()); + setDrawPlayers(config.debugDrawPlayers()); + drawSettings.setSelected(config.debugDrawSettings()); + setDrawSettings(config.debugDrawSettings()); + drawWeb.setSelected(config.debugDrawWeb()); + setDrawWeb(config.debugDrawWeb()); + } + + drawBoundaries.setEnabled(false); + drawGround.setEnabled(false); + drawInventory.setEnabled(false); + drawNPCs.setEnabled(false); + drawObjects.setEnabled(false); + drawPlayers.setEnabled(false); + + drawWeb.setEnabled(false); + + this.add(drawMouse); + this.add(drawMouseTrail); + this.add(enableMouse); + this.add(drawBoundaries); + this.add(drawGround); + this.add(drawInventory); + this.add(drawNPCs); + this.add(drawObjects); + this.add(drawPlayers); + this.add(drawSettings); + this.add(drawWeb); + this.add(logLevel); + } + + public void setLogLevel(Level level) { + bot.getScriptHandler().getDebugSettingsListener().setLogLevel(level); + } + + public void setDrawMouse(boolean drawMouse) { + bot.getScriptHandler().getDebugSettingsListener().setDrawMouse(drawMouse); + } + + public void setDrawMouseTrail(boolean drawMouseTrail) { + bot.getScriptHandler().getDebugSettingsListener().setDrawMouseTrail(drawMouseTrail); + } + + public void setEnableMouse(boolean enableMouse) { + bot.getScriptHandler().getDebugSettingsListener().setEnableMouse(enableMouse); + } + + public void setDrawBoundaries(boolean drawBoundaries) { + bot.getScriptHandler().getDebugSettingsListener().setDrawBoundaries(drawBoundaries); + } + + public void setDrawGround(boolean drawGround) { + bot.getScriptHandler().getDebugSettingsListener().setDrawGround(drawGround); + } + + public void setDrawInventory(boolean drawInventory) { + bot.getScriptHandler().getDebugSettingsListener().setDrawInventory(drawInventory); + } + + public void setDrawNPCs(boolean drawNPCs) { + bot.getScriptHandler().getDebugSettingsListener().setDrawNPCs(drawNPCs); + } + + public void setDrawObjects(boolean drawObjects) { + bot.getScriptHandler().getDebugSettingsListener().setDrawObjects(drawObjects); + } + + public void setDrawPlayers(boolean drawPlayers) { + bot.getScriptHandler().getDebugSettingsListener().setDrawPlayers(drawPlayers); + } + + public void setDrawSettings(boolean drawSettings) { + bot.getScriptHandler().getDebugSettingsListener().setDrawSettings(drawSettings); + } + + public void setDrawWeb(boolean drawWeb) { + bot.getScriptHandler().getDebugSettingsListener().setDrawWeb(drawWeb); + } +} diff --git a/src/main/java/net/runelite/client/plugins/bot/base/GeneralPanel.java b/src/main/java/net/runelite/client/plugins/bot/base/GeneralPanel.java new file mode 100644 index 00000000..c12f8a3d --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/bot/base/GeneralPanel.java @@ -0,0 +1,61 @@ +package net.runelite.client.plugins.bot.base; + +import net.runelite.rsb.botLauncher.Application; +import net.runelite.rsb.internal.globval.GlobalConfiguration; + +import javax.swing.*; + +public class GeneralPanel extends JPanel { + + private static GeneralPanel generalPanel; + JButton addBotButton = new JButton("Add bot"); + JButton openScriptsFolderButton = new JButton("Scripts Folder"); + + public static GeneralPanel getInstance() { + if (generalPanel == null) { + generalPanel = new GeneralPanel(); + } + return generalPanel; + } + + public GeneralPanel() { + super(); + generalPanel = this; + init(); + } + + public void addBotButtonAction() { + Application.addBot(true); + Application.setBot(Application.getBots().length - 1); + } + + + private void init() { + addBotButton.addActionListener(e -> addBotButtonAction()); + this.add(addBotButton); + openScriptsFolderButton.addActionListener(e -> openScriptsFolderPerformed()); + this.add(openScriptsFolderButton); + } + + /** + * Opens the scripts folder in the default file explorer + */ + private void openScriptsFolderPerformed() { + String folderPath = GlobalConfiguration.Paths.getScriptsPrecompiledDirectory(); + try { + switch (GlobalConfiguration.getCurrentOperatingSystem()) { + case WINDOWS: + Runtime.getRuntime().exec("explorer.exe " + folderPath); + break; + case LINUX: + Runtime.getRuntime().exec("xdg-open " + folderPath); + break; + case MAC: + Runtime.getRuntime().exec("open " + folderPath); + break; + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } +} diff --git a/src/main/java/net/runelite/rsb/botLauncher/Application.java b/src/main/java/net/runelite/rsb/botLauncher/Application.java index 3f0da981..2a90b6e9 100644 --- a/src/main/java/net/runelite/rsb/botLauncher/Application.java +++ b/src/main/java/net/runelite/rsb/botLauncher/Application.java @@ -9,8 +9,11 @@ import net.runelite.rsb.wrappers.common.CacheProvider; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.lang.reflect.Field; import java.util.*; +import java.util.Map; @Slf4j public class Application { @@ -28,14 +31,115 @@ public class Application { public static void main(final String[] args) throws Throwable { preParser = new ArgumentPreParser(args); if (preParser.contains("--bot-runelite")) { + loadJagexCredentials(); + String scriptName = preParser.consumeValue("--script"); addBot(preParser.contains("--headless")); - // checkForCacheAndLoad(); + if (scriptName != null) { + autoStartScript(scriptName); + } CLIHandler.handleCLI(); } else { net.runelite.client.RuneLite.main(args); } } + /** + * Auto-starts a script after the player is logged in. + * Polls game state in a background thread, waiting for LOGGED_IN before launching. + * + * @param scriptName Script name with spaces removed (e.g. "PixelBot-Woodcutting") + */ + private static void autoStartScript(String scriptName) { + new Thread(() -> { + log.info("Auto-start queued for script: {} — log in within 45 seconds", scriptName); + try { + // Wait for client to initialize and user to log in + Thread.sleep(45000); + log.info("Starting script: {}", scriptName); + BotLiteInterface bot = getBots()[0]; + bot.runScript("default", scriptName); + log.info("Auto-started script: {}", scriptName); + } catch (Exception e) { + log.error("Failed to auto-start script: {}", scriptName, e); + } + }, "ScriptAutoStart").start(); + } + + /** + * Loads Jagex Launcher credentials from ~/.runelite/credentials.properties + * and injects them as real environment variables so the game client + * can authenticate via System.getenv("JX_ACCESS_TOKEN") etc. + * + * The launcher scripts (launch-bot.bat/sh) also set these as env vars + * before starting Java. This method is a fallback for direct JAR launch. + * + * To generate credentials.properties: + * 1. Open "RuneLite (configure)" and add --insecure-write-credentials to client arguments + * 2. Launch RuneLite via Jagex Launcher and log in + * 3. credentials.properties will be created in ~/.runelite/ + */ + private static void loadJagexCredentials() { + File credFile = new File(System.getProperty("user.home"), ".runelite/credentials.properties"); + if (!credFile.exists()) { + log.info("No credentials.properties found at {}. See launch-bot.bat for setup instructions.", credFile.getAbsolutePath()); + return; + } + try (FileInputStream fis = new FileInputStream(credFile)) { + Properties creds = new Properties(); + creds.load(fis); + int envSet = 0; + for (Map.Entry entry : creds.entrySet()) { + String key = entry.getKey().toString(); + String value = entry.getValue().toString(); + // Set as system property (accessible via System.getProperty) + System.setProperty(key, value); + // Also inject as environment variable (accessible via System.getenv) + if (setEnvironmentVariable(key, value)) { + envSet++; + } + log.info("Loaded Jagex credential: {}", key); + } + log.info("Loaded {} credentials from {} ({} set as env vars)", creds.size(), credFile.getAbsolutePath(), envSet); + } catch (IOException e) { + log.warn("Failed to load credentials.properties", e); + } + } + + /** + * Injects a key-value pair into the current process's environment variables + * via reflection. This is needed because System.getenv() returns an unmodifiable + * map, but the RuneLite client reads JX_* tokens from environment variables. + * + * @return true if successfully set, false if reflection failed + */ + @SuppressWarnings("unchecked") + private static boolean setEnvironmentVariable(String key, String value) { + // Skip if already set (e.g. by launcher script) + if (value.equals(System.getenv(key))) { + return true; + } + try { + // On Windows, ProcessEnvironment has a theCaseInsensitiveEnvironment field + Class processEnvClass = Class.forName("java.lang.ProcessEnvironment"); + try { + Field theEnvironmentField = processEnvClass.getDeclaredField("theEnvironment"); + theEnvironmentField.setAccessible(true); + Map env = (Map) theEnvironmentField.get(null); + env.put(key, value); + } catch (NoSuchFieldException e) { + // Fallback: try the unmodifiable map's backing field + Map env = System.getenv(); + Field field = env.getClass().getDeclaredField("m"); + field.setAccessible(true); + ((Map) field.get(env)).put(key, value); + } + return true; + } catch (Exception e) { + log.debug("Could not set env var {} via reflection (expected on some JDKs). Use launch-bot.bat/sh instead.", key); + return false; + } + } + /** * Checks if the cache exists and if it does, loads it * if not it creates a new cache and saves it @@ -169,6 +273,24 @@ public boolean contains(Object o) { return within; } + /** + * Consumes a key-value argument pair (e.g. --script PixelBot-Woodcutting). + * Removes both the key and value from the list and returns the value. + * + * @param key The argument key (e.g. "--script") + * @return The value following the key, or null if not found + */ + public String consumeValue(String key) { + int index = indexOf(key); + if (index >= 0 && index + 1 < size()) { + remove(index); // remove key + return remove(index); // remove and return value (now at same index) + } else if (index >= 0) { + remove(index); // remove orphan key + } + return null; + } + } } diff --git a/src/main/java/net/runelite/rsb/botLauncher/BotLite.java b/src/main/java/net/runelite/rsb/botLauncher/BotLite.java index 666bd537..d1582245 100644 --- a/src/main/java/net/runelite/rsb/botLauncher/BotLite.java +++ b/src/main/java/net/runelite/rsb/botLauncher/BotLite.java @@ -17,13 +17,22 @@ import net.runelite.rsb.internal.input.Canvas; import net.runelite.rsb.methods.Environment; import net.runelite.rsb.methods.MethodContext; +import net.runelite.client.plugins.bot.BotPlugin; +import net.runelite.client.ui.ClientToolbar; +import net.runelite.client.ui.NavigationButton; +import net.runelite.client.plugins.bot.BotPanel; +import net.runelite.client.plugins.bot.AccountPanel; +import net.runelite.client.plugins.bot.ScriptPanel; +import net.runelite.client.plugins.bot.base.DebugPanel; import net.runelite.rsb.plugin.AccountManager; import net.runelite.rsb.plugin.ScriptSelector; import net.runelite.rsb.service.ScriptDefinition; -import java.applet.Applet; +import javax.imageio.ImageIO; +import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; +import java.io.InputStream; import java.lang.reflect.Constructor; import java.util.EventListener; import java.util.Map; @@ -100,7 +109,7 @@ public Client getClient() { return client = injector.getInstance(Client.class); } - public Applet getApplet() {return applet = injector.getInstance(Applet.class);} + // Applet API removed in RuneLite 1.12+ public ItemManager getItemManager() { return injector.getInstance(ItemManager.class);} @@ -250,8 +259,8 @@ public Canvas getCanvas() { return canvas; } - public Applet getLoader() { - return (Applet) this.getClient(); + public java.awt.Canvas getLoader() { + return this.getClient().getCanvas(); } /** @@ -267,8 +276,10 @@ public void setMethodContext() { * Stops and shuts down the current bot instance */ public void shutdown() { - getLoader().stop(); - getLoader().setVisible(false); + java.awt.Canvas loader = getLoader(); + if (loader != null) { + loader.setVisible(false); + } eventManager.killThread(false); sh.stopScript(); psh.stopScript(); @@ -299,6 +310,94 @@ public void init(boolean startClientBare) throws Exception { else { this.start(); } + // Register the bot sidebar panel after the UI is fully initialized. + // We delay this to ensure ClientUI.sidebar is created (avoids the NPE + // that occurs when plugins call addNavigation during early startup). + initBotPanel(); + } + + /** + * Manually initializes and registers the Bot sidebar panel with RuneLite's toolbar. + * This bypasses RuneLite's plugin discovery timing issues where the sidebar + * isn't ready when plugins call addNavigation() during startUp(). + */ + private void initBotPanel() { + SwingUtilities.invokeLater(() -> { + try { + BotLite bot = getInjectorInstance(); + BotPanel panel = injector.getInstance(BotPanel.class); + + AccountPanel accountPanel = new AccountPanel(bot); + ScriptPanel scriptPanel = new ScriptPanel(bot); + DebugPanel debugPanel = new DebugPanel(bot); + + panel.associateBot(accountPanel, scriptPanel, debugPanel); + + InputStream iconStream = BotPlugin.class.getResourceAsStream("rsb.png"); + BufferedImage icon; + if (iconStream != null) { + Image img = ImageIO.read(iconStream); + icon = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_RGB); + Graphics g = icon.getGraphics(); + g.drawImage(img, 0, 0, null); + g.dispose(); + } else { + log.warn("Could not load rsb.png icon for bot panel"); + icon = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB); + } + + NavigationButton navButton = NavigationButton.builder() + .tooltip("Bot Interface") + .icon(icon) + .priority(10) + .panel(panel) + .build(); + + ClientToolbar toolbar = injector.getInstance(ClientToolbar.class); + toolbar.addNavigation(navButton); + log.info("Bot panel registered in sidebar"); + } catch (Exception e) { + log.error("Failed to initialize bot panel — retrying in 3s", e); + // Retry once after a delay in case the sidebar isn't ready yet + new Thread(() -> { + try { + Thread.sleep(3000); + SwingUtilities.invokeLater(() -> { + try { + initBotPanelRetry(); + } catch (Exception ex) { + log.error("Bot panel retry also failed", ex); + } + }); + } catch (InterruptedException ignored) {} + }, "BotPanel-Retry").start(); + } + }); + } + + private void initBotPanelRetry() { + try { + BotLite bot = getInjectorInstance(); + BotPanel panel = injector.getInstance(BotPanel.class); + AccountPanel accountPanel = new AccountPanel(bot); + ScriptPanel scriptPanel = new ScriptPanel(bot); + DebugPanel debugPanel = new DebugPanel(bot); + panel.associateBot(accountPanel, scriptPanel, debugPanel); + + BufferedImage icon = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB); + NavigationButton navButton = NavigationButton.builder() + .tooltip("Bot Interface") + .icon(icon) + .priority(10) + .panel(panel) + .build(); + + ClientToolbar toolbar = injector.getInstance(ClientToolbar.class); + toolbar.addNavigation(navButton); + log.info("Bot panel registered in sidebar (retry succeeded)"); + } catch (Exception e) { + log.error("Bot panel registration failed on retry", e); + } } public BotLite() throws Exception { @@ -324,14 +423,18 @@ public BotLite() throws Exception { } public void runScript(String account, String scriptName) { - getInjectorInstance().setAccount(account); + try { + getInjectorInstance().setAccount(account); + } catch (Exception e) { + log.warn("Could not set account '{}', continuing without account: {}", account, e.getMessage()); + } ScriptSelector ss = new ScriptSelector(getInjectorInstance()); ss.load(); ScriptDefinition def = ss.getScripts().stream().filter(x -> x.name.replace(" ", "").equals(scriptName)).findFirst().get(); try { getInjectorInstance().getScriptHandler().runScript(def.source.load(def)); } catch (Exception e) { - e.printStackTrace(); + log.error("Failed to run script: {}", scriptName, e); } } diff --git a/src/main/java/net/runelite/rsb/internal/InputManager.java b/src/main/java/net/runelite/rsb/internal/InputManager.java index 2ce36988..dcbf3377 100644 --- a/src/main/java/net/runelite/rsb/internal/InputManager.java +++ b/src/main/java/net/runelite/rsb/internal/InputManager.java @@ -8,7 +8,7 @@ import net.runelite.rsb.internal.input.Canvas; import net.runelite.rsb.internal.input.VirtualMouse; -import java.applet.Applet; +import java.awt.*; import java.awt.event.*; @Slf4j @@ -88,7 +88,7 @@ private void gainFocus() { } private Canvas getCanvasWrapper() { - return (Canvas) getTarget().getComponent(0); + return bot.getCanvas(); } private Client getClient() { @@ -103,8 +103,13 @@ private char getKeyChar(final char c) { } } - private Applet getTarget() { - return (Applet) getClient(); + /** + * Returns the game canvas as the event source Component. + * In RuneLite 1.12+ the client no longer extends Applet, + * so we use the canvas directly. + */ + private Component getTarget() { + return bot.getLoader(); } public int getX() { diff --git a/src/main/java/net/runelite/rsb/internal/input/VirtualKeyboard.java b/src/main/java/net/runelite/rsb/internal/input/VirtualKeyboard.java index 491c07bd..64b8ee65 100644 --- a/src/main/java/net/runelite/rsb/internal/input/VirtualKeyboard.java +++ b/src/main/java/net/runelite/rsb/internal/input/VirtualKeyboard.java @@ -4,7 +4,6 @@ import net.runelite.client.input.KeyListener; import net.runelite.rsb.methods.MethodContext; -import java.applet.Applet; import java.awt.*; import java.awt.event.FocusEvent; import java.awt.event.KeyEvent; @@ -43,7 +42,7 @@ public void sendEvent(KeyEvent e){ keyReleased(e); } EventQueue eventQueue = Toolkit.getDefaultToolkit().getSystemEventQueue(); - eventQueue.postEvent(new FocusEvent( ((Applet) methods.client).getComponent(0), FocusEvent.FOCUS_GAINED)); + eventQueue.postEvent(new FocusEvent(methods.client.getCanvas(), FocusEvent.FOCUS_GAINED)); eventQueue.postEvent(e); } diff --git a/src/main/java/net/runelite/rsb/internal/input/VirtualMouse.java b/src/main/java/net/runelite/rsb/internal/input/VirtualMouse.java index 54b5df5f..f24a3aae 100644 --- a/src/main/java/net/runelite/rsb/internal/input/VirtualMouse.java +++ b/src/main/java/net/runelite/rsb/internal/input/VirtualMouse.java @@ -3,7 +3,6 @@ import lombok.extern.slf4j.Slf4j; import net.runelite.rsb.methods.MethodContext; -import java.applet.Applet; import java.awt.event.FocusEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; @@ -143,7 +142,7 @@ public final void sendEvent(MouseEvent e) { } else { throw new InternalError(e.toString()); } - ((Applet) methods.client).getComponent(0).dispatchEvent(e); + methods.client.getCanvas().dispatchEvent(e); } catch (NullPointerException ignored) { log.debug("Listener is being re-instantiated on the client", ignored); } diff --git a/src/main/java/net/runelite/rsb/methods/Objects.java b/src/main/java/net/runelite/rsb/methods/Objects.java index 0c5a7d89..f472286f 100644 --- a/src/main/java/net/runelite/rsb/methods/Objects.java +++ b/src/main/java/net/runelite/rsb/methods/Objects.java @@ -1,7 +1,6 @@ package net.runelite.rsb.methods; import net.runelite.api.*; -import net.runelite.cache.definitions.ObjectDefinition; import net.runelite.rsb.query.RSObjectQueryBuilder; import net.runelite.rsb.wrappers.RSObject; import net.runelite.rsb.wrappers.RSTile; @@ -196,10 +195,10 @@ public RSObject getNearest(final int... ids) { @Nullable public RSObject getNearest(final String... names) { return getNearest(o -> { - ObjectDefinition def = o.getDef(); - if (def != null) { + String objName = o.getName(); + if (!objName.isEmpty()) { for (String name : names) { - if (name.equals(def.getName())) { + if (name.equals(objName)) { return true; } } @@ -221,10 +220,10 @@ public RSObject getNearest(final String... names) { @Nullable public RSObject findNearest(final int distance, final String... names) { return getNearest(distance, o -> { - ObjectDefinition def = o.getDef(); - if (def != null) { + String objName = o.getName(); + if (!objName.isEmpty()) { for (String name : names) { - if (name.equals(def.getName())) { + if (name.equals(objName)) { return true; } } diff --git a/src/main/java/net/runelite/rsb/methods/Players.java b/src/main/java/net/runelite/rsb/methods/Players.java index 97ae051f..c5aa8335 100644 --- a/src/main/java/net/runelite/rsb/methods/Players.java +++ b/src/main/java/net/runelite/rsb/methods/Players.java @@ -55,7 +55,7 @@ public RSPlayer[] getAll() { * @return All valid RSPlayers. */ public RSPlayer[] getAll(final Filter filter) { - Player[] playerArray = methods.client.getCachedPlayers(); + Player[] playerArray = methods.client.getPlayers().toArray(new Player[0]); Set players = new HashSet<>(); for (Player player : playerArray) { if (player != null) { @@ -80,7 +80,7 @@ public RSPlayer[] getAll(final Filter filter) { public RSPlayer getNearest(final Filter filter) { int min = 20; RSPlayer closest = null; - Player[] players = methods.client.getCachedPlayers(); + Player[] players = methods.client.getPlayers().toArray(new Player[0]); for (Player player : players) { if (player == null) { continue; diff --git a/src/main/java/net/runelite/rsb/script/adaptive/ActionTracker.java b/src/main/java/net/runelite/rsb/script/adaptive/ActionTracker.java new file mode 100644 index 00000000..c89fb920 --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/ActionTracker.java @@ -0,0 +1,169 @@ +package net.runelite.rsb.script.adaptive; + +import net.runelite.rsb.script.adaptive.data.ActionRecord; +import net.runelite.rsb.script.adaptive.data.SessionSummary; +import net.runelite.rsb.script.adaptive.data.StrategyProfile; +import net.runelite.rsb.script.adaptive.stats.RunningStats; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Records actions, computes live XP/hr and actions/hr. + * Manages ActionHandle instances for begin/end tracking. + */ +public class ActionTracker { + private final Map strategies; + private final List sessionActions; + private final RunningStats durationStats; + private final long sessionStartTime; + + private int totalXpGained; + private int totalActions; + private int successfulActions; + private int lastSleepDuration; + + public ActionTracker(Map strategies) { + this.strategies = strategies; + this.sessionActions = new ArrayList<>(); + this.durationStats = new RunningStats(); + this.sessionStartTime = System.currentTimeMillis(); + this.totalXpGained = 0; + this.totalActions = 0; + this.successfulActions = 0; + this.lastSleepDuration = 0; + } + + /** + * Begins tracking a new action. Returns a handle to be passed to endAction(). + */ + public ActionHandle beginAction(String actionType, int targetId, String targetName) { + return new ActionHandle(actionType, targetId, targetName, System.currentTimeMillis(), lastSleepDuration); + } + + /** + * Ends a tracked action and records its result. + */ + public ActionRecord endAction(ActionHandle handle, boolean success, int xpGained) { + long endTime = System.currentTimeMillis(); + ActionRecord record = new ActionRecord( + handle.actionType, handle.targetId, handle.targetName, + handle.startTime, endTime, success, xpGained, handle.sleepBefore); + + sessionActions.add(record); + durationStats.addValue(record.getDuration()); + totalActions++; + if (success) { + successfulActions++; + totalXpGained += xpGained; + } + + // Update strategy profile + String key = record.getStrategyKey(); + StrategyProfile profile = strategies.computeIfAbsent(key, StrategyProfile::new); + profile.recordAction(record); + + return record; + } + + /** + * Records the last sleep duration for correlation with action success. + */ + public void recordSleep(int sleepMs) { + this.lastSleepDuration = sleepMs; + } + + /** + * Returns live XP per hour based on session elapsed time. + */ + public double getLiveXpPerHour() { + long elapsed = System.currentTimeMillis() - sessionStartTime; + if (elapsed <= 0) return 0; + return totalXpGained * 3600000.0 / elapsed; + } + + /** + * Returns live actions per hour based on session elapsed time. + */ + public double getLiveActionsPerHour() { + long elapsed = System.currentTimeMillis() - sessionStartTime; + if (elapsed <= 0) return 0; + return totalActions * 3600000.0 / elapsed; + } + + public double getLiveSuccessRate() { + return totalActions > 0 ? (double) successfulActions / totalActions : 0; + } + + public int getTotalActions() { + return totalActions; + } + + public int getSuccessfulActions() { + return successfulActions; + } + + public int getTotalXpGained() { + return totalXpGained; + } + + public long getSessionStartTime() { + return sessionStartTime; + } + + public long getSessionElapsedMs() { + return System.currentTimeMillis() - sessionStartTime; + } + + public RunningStats getDurationStats() { + return durationStats; + } + + public List getSessionActions() { + return sessionActions; + } + + /** + * Builds a SessionSummary from the current session data. + */ + public SessionSummary buildSessionSummary() { + SessionSummary summary = new SessionSummary( + sessionStartTime, System.currentTimeMillis(), + totalActions, successfulActions, totalXpGained); + summary.setAvgActionDuration(durationStats.getMean()); + return summary; + } + + /** + * Handle returned by beginAction(), passed to endAction() to complete tracking. + */ + public static class ActionHandle { + final String actionType; + final int targetId; + final String targetName; + final long startTime; + final int sleepBefore; + + ActionHandle(String actionType, int targetId, String targetName, + long startTime, int sleepBefore) { + this.actionType = actionType; + this.targetId = targetId; + this.targetName = targetName; + this.startTime = startTime; + this.sleepBefore = sleepBefore; + } + + public String getActionType() { + return actionType; + } + + public int getTargetId() { + return targetId; + } + + public long getStartTime() { + return startTime; + } + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/AdaptiveContext.java b/src/main/java/net/runelite/rsb/script/adaptive/AdaptiveContext.java new file mode 100644 index 00000000..cc6ba767 --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/AdaptiveContext.java @@ -0,0 +1,76 @@ +package net.runelite.rsb.script.adaptive; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.rsb.script.adaptive.data.LearningDataStore; +import net.runelite.rsb.script.adaptive.data.SessionSummary; +import net.runelite.rsb.script.adaptive.data.StrategyProfile; +import net.runelite.rsb.script.adaptive.paint.AdaptivePaintOverlay; + +import java.util.Map; + +/** + * Central hub that wires all adaptive components together. + * Created by AdaptiveScript on start, provides access to tracker, optimizer, sleep, chat, and store. + */ +@Slf4j +public class AdaptiveContext { + private final ActionTracker tracker; + private final StrategyOptimizer optimizer; + private final AdaptiveSleep sleep; + private final ChatReactor chat; + private final LearningDataStore store; + private final AdaptivePaintOverlay paint; + private final Map strategies; + + public AdaptiveContext(String scriptName, String accountName) { + this.store = new LearningDataStore(scriptName, accountName); + this.strategies = store.loadStrategies(); + this.tracker = new ActionTracker(strategies); + this.optimizer = new StrategyOptimizer(strategies); + this.sleep = new AdaptiveSleep(); + this.chat = new ChatReactor(); + this.paint = new AdaptivePaintOverlay(this); + + log.info("Adaptive context initialized for script={}, account={}", scriptName, accountName); + log.info("Loaded {} strategy profiles from {}", strategies.size(), store.getDataPath()); + } + + /** + * Saves all learning data to disk. Called on script finish. + */ + public void save() { + store.saveStrategies(strategies); + SessionSummary summary = tracker.buildSessionSummary(); + store.saveSession(summary); + log.info("Saved learning data: {} strategies, session: {} actions, {} XP", + strategies.size(), summary.getTotalActions(), summary.getTotalXpGained()); + } + + public ActionTracker getTracker() { + return tracker; + } + + public StrategyOptimizer getOptimizer() { + return optimizer; + } + + public AdaptiveSleep getSleep() { + return sleep; + } + + public ChatReactor getChat() { + return chat; + } + + public LearningDataStore getStore() { + return store; + } + + public AdaptivePaintOverlay getPaint() { + return paint; + } + + public Map getStrategies() { + return strategies; + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/AdaptiveScript.java b/src/main/java/net/runelite/rsb/script/adaptive/AdaptiveScript.java new file mode 100644 index 00000000..510d1ed1 --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/AdaptiveScript.java @@ -0,0 +1,216 @@ +package net.runelite.rsb.script.adaptive; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.rsb.event.events.MessageEvent; +import net.runelite.rsb.event.listener.MessageListener; +import net.runelite.rsb.event.listener.PaintListener; +import net.runelite.rsb.script.Script; +import net.runelite.rsb.script.ScriptManifest; + +import java.awt.*; +import java.util.HashSet; +import java.util.Set; + +/** + * Base class for adaptive scripts. Extends Script, implements PaintListener and MessageListener. + * Scripts opt in to the adaptive system by extending this class instead of Script. + * + * Provides convenience methods: + * - adaptiveSleep(context, min, max): generates optimized sleep durations + * - beginAction(type, targetId, name): starts tracking an action + * - endAction(handle, success, xpGained): completes action tracking + * + * Lifecycle: + * - onStart() loads persisted learning data + * - onFinish() saves learning data + * - messageReceived() delegates to ChatReactor + */ +@Slf4j +public abstract class AdaptiveScript extends Script implements PaintListener, MessageListener { + + protected AdaptiveContext adaptive; + + /** Whether auto-drop is enabled when inventory is full (default: true). */ + protected boolean autoDropEnabled = true; + + /** Item IDs to keep (never auto-drop). Tools, etc. */ + private final Set keepItemIds = new HashSet<>(); + + /** Item names to keep (never auto-drop). Case-insensitive matching. */ + private final Set keepItemNames = new HashSet<>(); + + /** + * Initializes the adaptive context and loads persisted data. + * Subclasses should call super.onStart() first. + */ + @Override + public boolean onStart() { + String scriptName = getScriptName(); + String accountName = getAccountName(); + adaptive = new AdaptiveContext(scriptName, accountName); + log.info("AdaptiveScript started: {}", scriptName); + return true; + } + + /** + * Saves learning data on script finish. + * Subclasses should call super.onFinish() to ensure data is persisted. + */ + @Override + public void onFinish() { + if (adaptive != null) { + adaptive.save(); + log.info("AdaptiveScript finished, data saved."); + } + } + + /** + * Delegates message events to the ChatReactor. + */ + @Override + public void messageReceived(MessageEvent e) { + if (adaptive != null) { + adaptive.getChat().onMessage(e); + } + } + + /** + * Renders the adaptive paint overlay. + */ + @Override + public void onRepaint(Graphics render) { + if (adaptive != null) { + adaptive.getPaint().render(render); + } + } + + // ---- Auto-drop ---- + + /** + * Checks if inventory is full and auto-drops if enabled. + * Called automatically, but scripts can also call this manually. + * + * @return true if items were dropped, false if no action was needed/taken + */ + protected boolean checkAndAutoDrop() { + if (!autoDropEnabled) return false; + if (!inventory.isFull() && !adaptive.getChat().isInventoryFull()) return false; + + log.info("Inventory full - auto-dropping items"); + resolveKeepNames(); + int[] keepIds = keepItemIds.stream().mapToInt(Integer::intValue).toArray(); + inventory.dropAllExcept(true, keepIds); + adaptive.getChat().clearInventoryFull(); + return true; + } + + /** + * Adds item IDs to the keep-list (these items will never be auto-dropped). + * Typically called in onStart() to protect tools, food, etc. + */ + protected void keepItems(int... ids) { + for (int id : ids) { + keepItemIds.add(id); + } + } + + /** + * Adds item names to the keep-list (case-insensitive, never auto-dropped). + * These are resolved to IDs at drop time. + */ + protected void keepItems(String... names) { + for (String name : names) { + keepItemNames.add(name.toLowerCase()); + } + } + + /** + * Clears the keep-list entirely. + */ + protected void clearKeepList() { + keepItemIds.clear(); + keepItemNames.clear(); + } + + /** + * Resolves keep-item names to IDs and merges with explicit keep IDs. + * Called internally before dropping. + */ + private void resolveKeepNames() { + if (keepItemNames.isEmpty()) return; + for (String name : keepItemNames) { + int id = inventory.getItemID(name); + if (id != -1) { + keepItemIds.add(id); + } + } + } + + // ---- Convenience methods ---- + + /** + * Returns an adaptive sleep duration based on learned timing data. + * Records the sleep for correlation tracking. + * + * @param context Label for this sleep context (e.g., "between_chops") + * @param minMs Minimum sleep in milliseconds + * @param maxMs Maximum sleep in milliseconds + * @return Sleep duration in milliseconds + */ + protected int adaptiveSleep(String context, int minMs, int maxMs) { + int duration = adaptive.getSleep().getSleepDuration(context, minMs, maxMs); + adaptive.getTracker().recordSleep(duration); + return duration; + } + + /** + * Begins tracking a new action. + * + * @param actionType Type of action (e.g., "chop_tree", "mine_rock") + * @param targetId Game object/NPC ID being targeted + * @param targetName Display name of the target + * @return ActionHandle to pass to endAction() + */ + protected ActionTracker.ActionHandle beginAction(String actionType, int targetId, String targetName) { + return adaptive.getTracker().beginAction(actionType, targetId, targetName); + } + + /** + * Ends a tracked action and records the result. + * If the action was successful with the preceding sleep, feeds that into sleep optimization. + * + * @param handle Handle from beginAction() + * @param success Whether the action succeeded + * @param xpGained XP gained from this action (0 if failed) + */ + protected void endAction(ActionTracker.ActionHandle handle, boolean success, int xpGained) { + adaptive.getTracker().endAction(handle, success, xpGained); + if (success) { + // Feed successful action timing back to sleep optimizer + adaptive.getSleep().recordSuccess(handle.getActionType(), (int) (handle.getStartTime() - System.currentTimeMillis())); + } + } + + /** + * Gets the script name from the ScriptManifest annotation, or class name as fallback. + */ + private String getScriptName() { + ScriptManifest manifest = getClass().getAnnotation(ScriptManifest.class); + if (manifest != null) { + return manifest.name(); + } + return getClass().getSimpleName(); + } + + /** + * Gets the current account name, or "default" if unavailable. + */ + private String getAccountName() { + try { + String name = account.getName(); + return name != null && !name.isEmpty() ? name : "default"; + } catch (Exception e) { + return "default"; + } + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/AdaptiveSleep.java b/src/main/java/net/runelite/rsb/script/adaptive/AdaptiveSleep.java new file mode 100644 index 00000000..142597b6 --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/AdaptiveSleep.java @@ -0,0 +1,93 @@ +package net.runelite.rsb.script.adaptive; + +import net.runelite.rsb.script.adaptive.stats.ExponentialMovingAverage; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +/** + * EMA-based delay optimization with Gaussian sampling. + * Tracks delays that preceded successful actions and learns optimal timing. + * Variance shrinks as sample count grows (wide exploration → tight optimization). + */ +public class AdaptiveSleep { + private static final double EMA_ALPHA = 0.1; + private static final int MIN_SAMPLES_FOR_ADAPTIVE = 10; + + private final Map sleepEmas; + private final Map sampleCounts; + private final Random random; + + public AdaptiveSleep() { + this.sleepEmas = new HashMap<>(); + this.sampleCounts = new HashMap<>(); + this.random = new Random(); + } + + /** + * Records a sleep duration that preceded a successful action. + */ + public void recordSuccess(String context, int sleepMs) { + ExponentialMovingAverage ema = sleepEmas.computeIfAbsent(context, + k -> new ExponentialMovingAverage(EMA_ALPHA)); + ema.addValue(sleepMs); + sampleCounts.merge(context, 1L, Long::sum); + } + + /** + * Generates an adaptive sleep duration. + * With few samples, uses uniform random between min and max. + * With enough data, uses Gaussian centered on learned optimal with shrinking variance. + * + * @param context A label for the sleep context (e.g., "between_chops", "post_drop") + * @param minMs Minimum sleep in milliseconds + * @param maxMs Maximum sleep in milliseconds + * @return Sleep duration in milliseconds + */ + public int getSleepDuration(String context, int minMs, int maxMs) { + long samples = sampleCounts.getOrDefault(context, 0L); + ExponentialMovingAverage ema = sleepEmas.get(context); + + if (samples < MIN_SAMPLES_FOR_ADAPTIVE || ema == null) { + // Not enough data: uniform random in [min, max] + return minMs + random.nextInt(Math.max(1, maxMs - minMs)); + } + + // Gaussian centered on EMA value, variance shrinks with more samples + double center = ema.getValue(); + double range = maxMs - minMs; + // Variance factor: starts at 0.3 of range, decays to 0.05 + double varianceFactor = Math.max(0.05, 0.3 * MIN_SAMPLES_FOR_ADAPTIVE / samples); + double stdDev = range * varianceFactor; + + double sample = center + random.nextGaussian() * stdDev; + + // Clamp to [min, max] + int result = (int) Math.round(Math.max(minMs, Math.min(maxMs, sample))); + return result; + } + + /** + * Returns the current learned optimal sleep for a context, or -1 if not enough data. + */ + public double getLearnedOptimal(String context) { + ExponentialMovingAverage ema = sleepEmas.get(context); + if (ema == null || ema.getCount() < MIN_SAMPLES_FOR_ADAPTIVE) return -1; + return ema.getValue(); + } + + /** + * Returns the sample count for a context. + */ + public long getSampleCount(String context) { + return sampleCounts.getOrDefault(context, 0L); + } + + /** + * Returns whether adaptive mode is active for a context. + */ + public boolean isAdaptive(String context) { + return sampleCounts.getOrDefault(context, 0L) >= MIN_SAMPLES_FOR_ADAPTIVE; + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/ChatReactor.java b/src/main/java/net/runelite/rsb/script/adaptive/ChatReactor.java new file mode 100644 index 00000000..3784e511 --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/ChatReactor.java @@ -0,0 +1,285 @@ +package net.runelite.rsb.script.adaptive; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.rsb.event.events.MessageEvent; + +import java.util.*; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +/** + * Reads chat messages and triggers behavior changes. + * Implements pattern-matching on game messages (MESSAGE_SERVER type = 0). + * Provides built-in rules for common skilling messages plus custom pattern support. + */ +@Slf4j +public class ChatReactor { + private static final int MAX_RECENT_MESSAGES = 50; + + // State flags + private volatile boolean inventoryFull; + private volatile boolean levelTooLow; + private volatile boolean missingTool; + private volatile boolean unreachable; + private volatile boolean loggedOut; + + // Last trigger message for debugging + private volatile String lastTrigger; + private volatile long lastTriggerTime; + + // Recent message log + private final Deque recentMessages; + + // Custom pattern handlers + private final List customHandlers; + + // Built-in patterns (compiled once) + private static final Pattern INVENTORY_FULL_PATTERN = Pattern.compile( + "(?i)(your inventory is too full|inventory is full|you can't carry any more|not enough space)"); + private static final Pattern LEVEL_TOO_LOW_PATTERN = Pattern.compile( + "(?i)(you need a higher|you need a .+ level of|you do not have the .+ level)"); + private static final Pattern MISSING_TOOL_PATTERN = Pattern.compile( + "(?i)(you don't have a|you need a .+ to|you do not have a)"); + private static final Pattern UNREACHABLE_PATTERN = Pattern.compile( + "(?i)(you can't reach that|i can't reach that|you can't get there)"); + private static final Pattern LOGGED_OUT_PATTERN = Pattern.compile( + "(?i)(you've been logged out|you have been disconnected)"); + + public ChatReactor() { + this.recentMessages = new ConcurrentLinkedDeque<>(); + this.customHandlers = new ArrayList<>(); + } + + /** + * Process an incoming message event. Called by AdaptiveScript's messageReceived(). + */ + public void onMessage(MessageEvent event) { + String message = event.getMessage(); + if (message == null || message.isEmpty()) return; + + // Store in recent messages + recentMessages.addLast(new TimestampedMessage( + System.currentTimeMillis(), event.getID(), event.getSender(), message)); + while (recentMessages.size() > MAX_RECENT_MESSAGES) { + recentMessages.removeFirst(); + } + + // Only process server messages for built-in patterns + if (event.getID() == MessageEvent.MESSAGE_SERVER || + event.getID() == MessageEvent.MESSAGE_ACTION) { + processBuiltInPatterns(message); + } + + // Process custom handlers for all message types + for (PatternHandler handler : customHandlers) { + if (handler.matches(event)) { + try { + handler.callback.accept(event); + } catch (Exception e) { + log.warn("Custom chat handler error: {}", e.getMessage()); + } + } + } + } + + private void processBuiltInPatterns(String message) { + if (INVENTORY_FULL_PATTERN.matcher(message).find()) { + inventoryFull = true; + setLastTrigger(message); + } + if (LEVEL_TOO_LOW_PATTERN.matcher(message).find()) { + levelTooLow = true; + setLastTrigger(message); + } + if (MISSING_TOOL_PATTERN.matcher(message).find()) { + missingTool = true; + setLastTrigger(message); + } + if (UNREACHABLE_PATTERN.matcher(message).find()) { + unreachable = true; + setLastTrigger(message); + } + if (LOGGED_OUT_PATTERN.matcher(message).find()) { + loggedOut = true; + setLastTrigger(message); + } + } + + private void setLastTrigger(String message) { + lastTrigger = message; + lastTriggerTime = System.currentTimeMillis(); + } + + /** + * Registers a custom message pattern handler. + * + * @param pattern Regex pattern to match against message text + * @param callback Consumer called with the MessageEvent when pattern matches + */ + public void onMessage(String pattern, Consumer callback) { + customHandlers.add(new PatternHandler(Pattern.compile(pattern, Pattern.CASE_INSENSITIVE), callback)); + } + + /** + * Registers a custom handler that also filters by message type. + */ + public void onMessage(String pattern, int messageType, Consumer callback) { + customHandlers.add(new PatternHandler(Pattern.compile(pattern, Pattern.CASE_INSENSITIVE), messageType, callback)); + } + + // ---- State flag getters ---- + + public boolean isInventoryFull() { + return inventoryFull; + } + + public boolean isLevelTooLow() { + return levelTooLow; + } + + public boolean isMissingTool() { + return missingTool; + } + + public boolean isUnreachable() { + return unreachable; + } + + public boolean isLoggedOut() { + return loggedOut; + } + + public String getLastTrigger() { + return lastTrigger; + } + + public long getLastTriggerTime() { + return lastTriggerTime; + } + + // ---- Flag clearing ---- + + public void clearInventoryFull() { + inventoryFull = false; + } + + public void clearLevelTooLow() { + levelTooLow = false; + } + + public void clearMissingTool() { + missingTool = false; + } + + public void clearUnreachable() { + unreachable = false; + } + + public void clearLoggedOut() { + loggedOut = false; + } + + public void clearAllFlags() { + inventoryFull = false; + levelTooLow = false; + missingTool = false; + unreachable = false; + loggedOut = false; + } + + // ---- Recent messages ---- + + /** + * Returns the most recent messages (newest last). + */ + public List getRecentMessages() { + return new ArrayList<>(recentMessages); + } + + /** + * Returns the most recent N messages. + */ + public List getRecentMessages(int count) { + List all = new ArrayList<>(recentMessages); + if (all.size() <= count) return all; + return all.subList(all.size() - count, all.size()); + } + + /** + * Checks if any recent message matches a pattern (within last N seconds). + */ + public boolean hasRecentMessage(String pattern, int withinSeconds) { + long cutoff = System.currentTimeMillis() - (withinSeconds * 1000L); + Pattern compiled = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + for (TimestampedMessage msg : recentMessages) { + if (msg.timestamp >= cutoff && compiled.matcher(msg.message).find()) { + return true; + } + } + return false; + } + + /** + * Returns the count of active flags. + */ + public int getActiveFlagCount() { + int count = 0; + if (inventoryFull) count++; + if (levelTooLow) count++; + if (missingTool) count++; + if (unreachable) count++; + if (loggedOut) count++; + return count; + } + + /** + * Returns a summary of active flags for display. + */ + public String getActiveFlagsSummary() { + List active = new ArrayList<>(); + if (inventoryFull) active.add("InvFull"); + if (levelTooLow) active.add("LvlLow"); + if (missingTool) active.add("NoTool"); + if (unreachable) active.add("NoPath"); + if (loggedOut) active.add("LoggedOut"); + return active.isEmpty() ? "None" : String.join(", ", active); + } + + // ---- Inner classes ---- + + public static class TimestampedMessage { + public final long timestamp; + public final int type; + public final String sender; + public final String message; + + public TimestampedMessage(long timestamp, int type, String sender, String message) { + this.timestamp = timestamp; + this.type = type; + this.sender = sender; + this.message = message; + } + } + + private static class PatternHandler { + final Pattern pattern; + final int messageType; // -1 means any type + final Consumer callback; + + PatternHandler(Pattern pattern, Consumer callback) { + this(pattern, -1, callback); + } + + PatternHandler(Pattern pattern, int messageType, Consumer callback) { + this.pattern = pattern; + this.messageType = messageType; + this.callback = callback; + } + + boolean matches(MessageEvent event) { + if (messageType >= 0 && event.getID() != messageType) return false; + return pattern.matcher(event.getMessage()).find(); + } + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/StrategyOptimizer.java b/src/main/java/net/runelite/rsb/script/adaptive/StrategyOptimizer.java new file mode 100644 index 00000000..16418261 --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/StrategyOptimizer.java @@ -0,0 +1,143 @@ +package net.runelite.rsb.script.adaptive; + +import net.runelite.rsb.script.adaptive.data.StrategyProfile; + +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * UCB1 + epsilon-greedy strategy selection. + * Balances exploration of new strategies with exploitation of known-good ones. + */ +public class StrategyOptimizer { + private static final int MIN_SAMPLES_FOR_EXPLOIT = 5; + private static final double INITIAL_EPSILON = 0.35; + private static final double MIN_EPSILON = 0.05; + private static final double EPSILON_DECAY_RATE = 50.0; // samples until epsilon halves + + private final Map strategies; + private final Random random; + private long totalSelections; + + public StrategyOptimizer(Map strategies) { + this.strategies = strategies; + this.random = new Random(); + this.totalSelections = 0; + + // Initialize totalSelections from existing data + for (StrategyProfile profile : strategies.values()) { + totalSelections += profile.getTotalAttempts(); + } + } + + /** + * Selects a strategy from the candidates using UCB1 + epsilon-greedy. + * + * @param candidates List of strategy keys (e.g., "chop_tree:1276") + * @param epsilon Exploration rate (0 = pure exploitation, 1 = pure exploration) + * @return The selected strategy key + */ + public String selectStrategy(List candidates, double epsilon) { + if (candidates == null || candidates.isEmpty()) { + throw new IllegalArgumentException("Candidates list cannot be empty"); + } + if (candidates.size() == 1) { + totalSelections++; + return candidates.get(0); + } + + // Force-explore strategies with insufficient data + for (String candidate : candidates) { + StrategyProfile profile = strategies.get(candidate); + if (profile == null || profile.getTotalAttempts() < MIN_SAMPLES_FOR_EXPLOIT) { + totalSelections++; + return candidate; + } + } + + // Epsilon-greedy: explore randomly with probability epsilon + if (random.nextDouble() < epsilon) { + totalSelections++; + return candidates.get(random.nextInt(candidates.size())); + } + + // UCB1 exploitation: pick the strategy with the highest UCB1 score + String best = null; + double bestScore = Double.NEGATIVE_INFINITY; + + for (String candidate : candidates) { + double score = computeUCB1Score(candidate); + if (score > bestScore) { + bestScore = score; + best = candidate; + } + } + + totalSelections++; + return best != null ? best : candidates.get(0); + } + + /** + * Computes the UCB1 score for a strategy. + * UCB1 = mean_reward + C * sqrt(ln(total_selections) / strategy_selections) + */ + private double computeUCB1Score(String strategyKey) { + StrategyProfile profile = strategies.get(strategyKey); + if (profile == null || profile.getTotalAttempts() == 0) { + return Double.MAX_VALUE; // Unexplored = highest priority + } + + // Normalize XP/hr to [0, 1] using best known XP/hr + double maxXpPerHour = 0; + for (StrategyProfile p : strategies.values()) { + if (p.getXpPerHour() > maxXpPerHour) { + maxXpPerHour = p.getXpPerHour(); + } + } + + double normalizedReward = maxXpPerHour > 0 ? profile.getXpPerHour() / maxXpPerHour : 0; + // Weight by success rate + double reward = normalizedReward * 0.7 + profile.getSuccessRate() * 0.3; + + double exploration = Math.sqrt(Math.log(Math.max(1, totalSelections)) / profile.getTotalAttempts()); + double C = 1.414; // sqrt(2), standard UCB1 constant + + return reward + C * exploration; + } + + /** + * Returns the recommended epsilon based on total data accumulated. + * Starts at ~0.35, decays to ~0.05 as data grows. + */ + public double getRecommendedEpsilon() { + double decay = Math.exp(-totalSelections / EPSILON_DECAY_RATE); + return MIN_EPSILON + (INITIAL_EPSILON - MIN_EPSILON) * decay; + } + + /** + * Returns a confidence level string based on data quality. + */ + public String getConfidenceLevel() { + if (totalSelections < 10) return "Very Low"; + if (totalSelections < 30) return "Low"; + if (totalSelections < 100) return "Medium"; + if (totalSelections < 300) return "High"; + return "Very High"; + } + + /** + * Returns the exploration rate as a percentage string. + */ + public String getExploreRateDisplay() { + return String.format("%.0f%%", getRecommendedEpsilon() * 100); + } + + public long getTotalSelections() { + return totalSelections; + } + + public StrategyProfile getProfile(String strategyKey) { + return strategies.get(strategyKey); + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/data/ActionRecord.java b/src/main/java/net/runelite/rsb/script/adaptive/data/ActionRecord.java new file mode 100644 index 00000000..b3b909e4 --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/data/ActionRecord.java @@ -0,0 +1,73 @@ +package net.runelite.rsb.script.adaptive.data; + +/** + * POJO representing a single recorded action. + */ +public class ActionRecord { + private final String actionType; + private final int targetId; + private final String targetName; + private final long startTime; + private final long endTime; + private final long duration; + private final boolean success; + private final int xpGained; + private final int sleepBefore; + + public ActionRecord(String actionType, int targetId, String targetName, + long startTime, long endTime, boolean success, + int xpGained, int sleepBefore) { + this.actionType = actionType; + this.targetId = targetId; + this.targetName = targetName; + this.startTime = startTime; + this.endTime = endTime; + this.duration = endTime - startTime; + this.success = success; + this.xpGained = xpGained; + this.sleepBefore = sleepBefore; + } + + public String getActionType() { + return actionType; + } + + public int getTargetId() { + return targetId; + } + + public String getTargetName() { + return targetName; + } + + public long getStartTime() { + return startTime; + } + + public long getEndTime() { + return endTime; + } + + public long getDuration() { + return duration; + } + + public boolean isSuccess() { + return success; + } + + public int getXpGained() { + return xpGained; + } + + public int getSleepBefore() { + return sleepBefore; + } + + /** + * Returns the strategy key for this action: "actionType:targetId" + */ + public String getStrategyKey() { + return actionType + ":" + targetId; + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/data/LearningDataStore.java b/src/main/java/net/runelite/rsb/script/adaptive/data/LearningDataStore.java new file mode 100644 index 00000000..c4705d04 --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/data/LearningDataStore.java @@ -0,0 +1,124 @@ +package net.runelite.rsb.script.adaptive.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import lombok.extern.slf4j.Slf4j; +import net.runelite.rsb.internal.globval.GlobalConfiguration; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * JSON persistence for learning data via Gson. + * Stores data at {OsrsBotDir}/learning/{scriptName}/{accountName}/ + */ +@Slf4j +public class LearningDataStore { + private static final String LEARNING_DIR = "learning"; + private static final String STRATEGIES_FILE = "strategies.json"; + private static final String SESSIONS_FILE = "sessions.json"; + private static final int MAX_SESSIONS = 100; + + private final Gson gson; + private final File dataDir; + + public LearningDataStore(String scriptName, String accountName) { + this.gson = new GsonBuilder().setPrettyPrinting().create(); + + String safeName = sanitizeFileName(scriptName); + String safeAccount = sanitizeFileName(accountName != null ? accountName : "default"); + + this.dataDir = new File(GlobalConfiguration.Paths.getOsrsBotDirectory() + + File.separator + LEARNING_DIR + + File.separator + safeName + + File.separator + safeAccount); + } + + /** + * Loads persisted strategy profiles. + */ + public Map loadStrategies() { + File file = new File(dataDir, STRATEGIES_FILE); + if (!file.exists()) { + return new HashMap<>(); + } + try (Reader reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) { + Type type = new TypeToken>() {}.getType(); + Map result = gson.fromJson(reader, type); + return result != null ? result : new HashMap<>(); + } catch (Exception e) { + log.warn("Failed to load strategies from {}: {}", file.getPath(), e.getMessage()); + return new HashMap<>(); + } + } + + /** + * Saves strategy profiles to disk. + */ + public void saveStrategies(Map strategies) { + ensureDirectory(); + File file = new File(dataDir, STRATEGIES_FILE); + try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) { + gson.toJson(strategies, writer); + } catch (Exception e) { + log.warn("Failed to save strategies to {}: {}", file.getPath(), e.getMessage()); + } + } + + /** + * Loads persisted session summaries. + */ + public List loadSessions() { + File file = new File(dataDir, SESSIONS_FILE); + if (!file.exists()) { + return new ArrayList<>(); + } + try (Reader reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) { + Type type = new TypeToken>() {}.getType(); + List result = gson.fromJson(reader, type); + return result != null ? result : new ArrayList<>(); + } catch (Exception e) { + log.warn("Failed to load sessions from {}: {}", file.getPath(), e.getMessage()); + return new ArrayList<>(); + } + } + + /** + * Appends a session summary, keeping only the last MAX_SESSIONS entries. + */ + public void saveSession(SessionSummary session) { + List sessions = loadSessions(); + sessions.add(session); + // Trim to max size + while (sessions.size() > MAX_SESSIONS) { + sessions.remove(0); + } + ensureDirectory(); + File file = new File(dataDir, SESSIONS_FILE); + try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) { + gson.toJson(sessions, writer); + } catch (Exception e) { + log.warn("Failed to save sessions to {}: {}", file.getPath(), e.getMessage()); + } + } + + /** + * Returns the data directory path (for logging/debugging). + */ + public String getDataPath() { + return dataDir.getAbsolutePath(); + } + + private void ensureDirectory() { + if (!dataDir.exists()) { + dataDir.mkdirs(); + } + } + + private String sanitizeFileName(String name) { + return name.replaceAll("[^a-zA-Z0-9._-]", "_"); + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/data/SessionSummary.java b/src/main/java/net/runelite/rsb/script/adaptive/data/SessionSummary.java new file mode 100644 index 00000000..2a644f9e --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/data/SessionSummary.java @@ -0,0 +1,81 @@ +package net.runelite.rsb.script.adaptive.data; + +/** + * POJO representing aggregated session statistics. + */ +public class SessionSummary { + private long startTime; + private long endTime; + private int totalActions; + private int successfulActions; + private int totalXpGained; + private double actionsPerHour; + private double xpPerHour; + private double avgActionDuration; + private double successRate; + + public SessionSummary() { + } + + public SessionSummary(long startTime, long endTime, int totalActions, + int successfulActions, int totalXpGained) { + this.startTime = startTime; + this.endTime = endTime; + this.totalActions = totalActions; + this.successfulActions = successfulActions; + this.totalXpGained = totalXpGained; + computeRates(); + } + + private void computeRates() { + long durationMs = endTime - startTime; + double hours = durationMs / 3600000.0; + this.actionsPerHour = hours > 0 ? totalActions / hours : 0; + this.xpPerHour = hours > 0 ? totalXpGained / hours : 0; + this.successRate = totalActions > 0 ? (double) successfulActions / totalActions : 0; + } + + public long getStartTime() { + return startTime; + } + + public long getEndTime() { + return endTime; + } + + public int getTotalActions() { + return totalActions; + } + + public int getSuccessfulActions() { + return successfulActions; + } + + public int getTotalXpGained() { + return totalXpGained; + } + + public double getActionsPerHour() { + return actionsPerHour; + } + + public double getXpPerHour() { + return xpPerHour; + } + + public double getAvgActionDuration() { + return avgActionDuration; + } + + public void setAvgActionDuration(double avgActionDuration) { + this.avgActionDuration = avgActionDuration; + } + + public double getSuccessRate() { + return successRate; + } + + public long getDurationMs() { + return endTime - startTime; + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/data/StrategyProfile.java b/src/main/java/net/runelite/rsb/script/adaptive/data/StrategyProfile.java new file mode 100644 index 00000000..64a9d62b --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/data/StrategyProfile.java @@ -0,0 +1,113 @@ +package net.runelite.rsb.script.adaptive.data; + +import net.runelite.rsb.script.adaptive.stats.RunningStats; + +/** + * Per-strategy accumulated stats with computed getters for XP/hr, success rate, etc. + * Persisted across sessions via LearningDataStore. + */ +public class StrategyProfile { + private String strategyKey; + private long totalAttempts; + private long totalSuccesses; + private long totalXpGained; + private long totalDurationMs; + private double avgDuration; + private double avgXpPerAction; + private double bestXpPerHour; + + // Transient (not serialized) - rebuilt from persisted values + private transient RunningStats durationStats; + + public StrategyProfile() { + this.durationStats = new RunningStats(); + } + + public StrategyProfile(String strategyKey) { + this.strategyKey = strategyKey; + this.totalAttempts = 0; + this.totalSuccesses = 0; + this.totalXpGained = 0; + this.totalDurationMs = 0; + this.avgDuration = 0; + this.avgXpPerAction = 0; + this.bestXpPerHour = 0; + this.durationStats = new RunningStats(); + } + + /** + * Records a completed action into this strategy profile. + */ + public void recordAction(ActionRecord record) { + totalAttempts++; + totalDurationMs += record.getDuration(); + if (record.isSuccess()) { + totalSuccesses++; + totalXpGained += record.getXpGained(); + } + ensureDurationStats(); + durationStats.addValue(record.getDuration()); + + // Update computed averages + avgDuration = totalAttempts > 0 ? (double) totalDurationMs / totalAttempts : 0; + avgXpPerAction = totalSuccesses > 0 ? (double) totalXpGained / totalSuccesses : 0; + + // Update best XP/hr if current rate is higher + double currentXpPerHour = getXpPerHour(); + if (currentXpPerHour > bestXpPerHour) { + bestXpPerHour = currentXpPerHour; + } + } + + public String getStrategyKey() { + return strategyKey; + } + + public long getTotalAttempts() { + return totalAttempts; + } + + public long getTotalSuccesses() { + return totalSuccesses; + } + + public long getTotalXpGained() { + return totalXpGained; + } + + public double getSuccessRate() { + return totalAttempts > 0 ? (double) totalSuccesses / totalAttempts : 0.0; + } + + public double getAvgDuration() { + return avgDuration; + } + + public double getAvgXpPerAction() { + return avgXpPerAction; + } + + /** + * Computes XP/hr based on average duration and XP per successful action. + */ + public double getXpPerHour() { + if (avgDuration <= 0 || totalSuccesses == 0) return 0.0; + double actionsPerHour = 3600000.0 / avgDuration; + return actionsPerHour * getSuccessRate() * avgXpPerAction; + } + + public double getBestXpPerHour() { + return bestXpPerHour; + } + + public RunningStats getDurationStats() { + ensureDurationStats(); + return durationStats; + } + + private void ensureDurationStats() { + if (durationStats == null) { + durationStats = new RunningStats(); + } + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/paint/AdaptivePaintOverlay.java b/src/main/java/net/runelite/rsb/script/adaptive/paint/AdaptivePaintOverlay.java new file mode 100644 index 00000000..08414898 --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/paint/AdaptivePaintOverlay.java @@ -0,0 +1,133 @@ +package net.runelite.rsb.script.adaptive.paint; + +import net.runelite.rsb.script.adaptive.AdaptiveContext; +import net.runelite.rsb.script.adaptive.ActionTracker; +import net.runelite.rsb.script.adaptive.StrategyOptimizer; +import net.runelite.rsb.script.adaptive.ChatReactor; + +import java.awt.*; + +/** + * Renders an adaptive stats overlay on the game screen. + * Shows runtime, XP/hr, actions/hr, explore rate, confidence, and chat status. + */ +public class AdaptivePaintOverlay { + private static final int X = 10; + private static final int Y = 340; + private static final int LINE_HEIGHT = 16; + private static final int BOX_WIDTH = 220; + + private static final Color BG_COLOR = new Color(0, 0, 0, 180); + private static final Color BORDER_COLOR = new Color(200, 170, 0); + private static final Color TITLE_COLOR = new Color(255, 215, 0); + private static final Color LABEL_COLOR = new Color(200, 200, 200); + private static final Color VALUE_COLOR = Color.WHITE; + private static final Color ALERT_COLOR = new Color(255, 80, 80); + private static final Color SUCCESS_COLOR = new Color(80, 255, 80); + + private final AdaptiveContext context; + + public AdaptivePaintOverlay(AdaptiveContext context) { + this.context = context; + } + + public void render(Graphics g) { + ActionTracker tracker = context.getTracker(); + StrategyOptimizer optimizer = context.getOptimizer(); + ChatReactor chat = context.getChat(); + + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // Count lines to determine box height + int lineCount = 9; // base lines + if (chat.getActiveFlagCount() > 0) lineCount++; + + int boxHeight = LINE_HEIGHT * lineCount + 12; + + // Background + g2.setColor(BG_COLOR); + g2.fillRoundRect(X, Y, BOX_WIDTH, boxHeight, 8, 8); + g2.setColor(BORDER_COLOR); + g2.drawRoundRect(X, Y, BOX_WIDTH, boxHeight, 8, 8); + + int textX = X + 8; + int textY = Y + LINE_HEIGHT; + + // Title + g2.setFont(new Font("Arial", Font.BOLD, 12)); + g2.setColor(TITLE_COLOR); + g2.drawString("Adaptive Bot", textX, textY); + textY += LINE_HEIGHT; + + // Runtime + g2.setFont(new Font("Arial", Font.PLAIN, 11)); + drawLabelValue(g2, textX, textY, "Runtime:", formatTime(tracker.getSessionElapsedMs())); + textY += LINE_HEIGHT; + + // XP/hr + drawLabelValue(g2, textX, textY, "XP/hr:", formatNumber(tracker.getLiveXpPerHour())); + textY += LINE_HEIGHT; + + // Actions/hr + drawLabelValue(g2, textX, textY, "Actions/hr:", formatNumber(tracker.getLiveActionsPerHour())); + textY += LINE_HEIGHT; + + // Actions (success/total) + String actionsStr = tracker.getSuccessfulActions() + "/" + tracker.getTotalActions(); + drawLabelValue(g2, textX, textY, "Actions:", actionsStr); + textY += LINE_HEIGHT; + + // Success rate + double rate = tracker.getLiveSuccessRate(); + g2.setColor(LABEL_COLOR); + g2.drawString("Success:", textX, textY); + g2.setColor(rate >= 0.8 ? SUCCESS_COLOR : rate >= 0.5 ? VALUE_COLOR : ALERT_COLOR); + g2.drawString(String.format("%.1f%%", rate * 100), textX + 80, textY); + textY += LINE_HEIGHT; + + // Explore rate + drawLabelValue(g2, textX, textY, "Explore:", optimizer.getExploreRateDisplay()); + textY += LINE_HEIGHT; + + // Confidence + drawLabelValue(g2, textX, textY, "Confidence:", optimizer.getConfidenceLevel()); + textY += LINE_HEIGHT; + + // Total XP + drawLabelValue(g2, textX, textY, "Total XP:", formatNumber(tracker.getTotalXpGained())); + textY += LINE_HEIGHT; + + // Chat flags (only if active) + if (chat.getActiveFlagCount() > 0) { + g2.setColor(LABEL_COLOR); + g2.drawString("Alerts:", textX, textY); + g2.setColor(ALERT_COLOR); + g2.drawString(chat.getActiveFlagsSummary(), textX + 80, textY); + } + } + + private void drawLabelValue(Graphics2D g2, int x, int y, String label, String value) { + g2.setColor(LABEL_COLOR); + g2.drawString(label, x, y); + g2.setColor(VALUE_COLOR); + g2.drawString(value, x + 80, y); + } + + private static String formatTime(long ms) { + long seconds = ms / 1000; + long hours = seconds / 3600; + long minutes = (seconds % 3600) / 60; + long secs = seconds % 60; + return String.format("%02d:%02d:%02d", hours, minutes, secs); + } + + private static String formatNumber(double value) { + if (value >= 1_000_000) { + return String.format("%.1fM", value / 1_000_000); + } else if (value >= 1_000) { + return String.format("%.1fK", value / 1_000); + } + return String.format("%.0f", value); + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/stats/ExponentialMovingAverage.java b/src/main/java/net/runelite/rsb/script/adaptive/stats/ExponentialMovingAverage.java new file mode 100644 index 00000000..e19000e5 --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/stats/ExponentialMovingAverage.java @@ -0,0 +1,54 @@ +package net.runelite.rsb.script.adaptive.stats; + +/** + * Exponential Moving Average with configurable smoothing factor (alpha). + * Higher alpha = more weight on recent values. + */ +public class ExponentialMovingAverage { + private final double alpha; + private double value; + private long count; + + public ExponentialMovingAverage(double alpha) { + if (alpha <= 0.0 || alpha > 1.0) { + throw new IllegalArgumentException("Alpha must be in (0, 1]: " + alpha); + } + this.alpha = alpha; + this.value = 0.0; + this.count = 0; + } + + public void addValue(double sample) { + if (count == 0) { + value = sample; + } else { + value = alpha * sample + (1.0 - alpha) * value; + } + count++; + } + + public double getValue() { + return value; + } + + public long getCount() { + return count; + } + + public double getAlpha() { + return alpha; + } + + public void reset() { + value = 0.0; + count = 0; + } + + /** + * Initializes the EMA with a pre-existing value and count (for persistence). + */ + public void initialize(double value, long count) { + this.value = value; + this.count = count; + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/stats/PercentileEstimator.java b/src/main/java/net/runelite/rsb/script/adaptive/stats/PercentileEstimator.java new file mode 100644 index 00000000..f46e9c58 --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/stats/PercentileEstimator.java @@ -0,0 +1,72 @@ +package net.runelite.rsb.script.adaptive.stats; + +import java.util.Arrays; + +/** + * Sorted-window percentile lookup using a circular buffer. + * Keeps the last N values and can compute any percentile on demand. + */ +public class PercentileEstimator { + private final double[] buffer; + private final int windowSize; + private int head; + private int count; + + public PercentileEstimator(int windowSize) { + if (windowSize <= 0) { + throw new IllegalArgumentException("Window size must be positive: " + windowSize); + } + this.windowSize = windowSize; + this.buffer = new double[windowSize]; + this.head = 0; + this.count = 0; + } + + public void addValue(double value) { + buffer[head] = value; + head = (head + 1) % windowSize; + if (count < windowSize) { + count++; + } + } + + /** + * Returns the value at the given percentile (0-100). + * Uses nearest-rank method. + */ + public double getPercentile(double percentile) { + if (count == 0) return 0.0; + if (percentile < 0 || percentile > 100) { + throw new IllegalArgumentException("Percentile must be in [0, 100]: " + percentile); + } + + double[] sorted = getSortedValues(); + int rank = (int) Math.ceil(percentile / 100.0 * sorted.length) - 1; + rank = Math.max(0, Math.min(rank, sorted.length - 1)); + return sorted[rank]; + } + + public double getMedian() { + return getPercentile(50); + } + + public int getCount() { + return count; + } + + private double[] getSortedValues() { + double[] values = new double[count]; + for (int i = 0; i < count; i++) { + // Read from the buffer, handling wrap-around + int idx = (head - count + i + windowSize) % windowSize; + values[i] = buffer[idx]; + } + Arrays.sort(values); + return values; + } + + public void reset() { + head = 0; + count = 0; + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/stats/RunningStats.java b/src/main/java/net/runelite/rsb/script/adaptive/stats/RunningStats.java new file mode 100644 index 00000000..8fbe98fe --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/stats/RunningStats.java @@ -0,0 +1,89 @@ +package net.runelite.rsb.script.adaptive.stats; + +/** + * Welford's online algorithm for computing running mean, variance, min, and max + * in a single pass with O(1) memory. + */ +public class RunningStats { + private long count; + private double mean; + private double m2; + private double min; + private double max; + + public RunningStats() { + this.count = 0; + this.mean = 0.0; + this.m2 = 0.0; + this.min = Double.MAX_VALUE; + this.max = Double.MIN_VALUE; + } + + public void addValue(double value) { + count++; + double delta = value - mean; + mean += delta / count; + double delta2 = value - mean; + m2 += delta * delta2; + + if (value < min) min = value; + if (value > max) max = value; + } + + public long getCount() { + return count; + } + + public double getMean() { + return count > 0 ? mean : 0.0; + } + + public double getVariance() { + return count > 1 ? m2 / (count - 1) : 0.0; + } + + public double getStdDev() { + return Math.sqrt(getVariance()); + } + + public double getMin() { + return count > 0 ? min : 0.0; + } + + public double getMax() { + return count > 0 ? max : 0.0; + } + + /** + * Merges another RunningStats into this one (for combining session data). + */ + public void merge(RunningStats other) { + if (other.count == 0) return; + if (this.count == 0) { + this.count = other.count; + this.mean = other.mean; + this.m2 = other.m2; + this.min = other.min; + this.max = other.max; + return; + } + long combinedCount = this.count + other.count; + double delta = other.mean - this.mean; + double combinedMean = this.mean + delta * other.count / combinedCount; + double combinedM2 = this.m2 + other.m2 + delta * delta * this.count * other.count / combinedCount; + + this.count = combinedCount; + this.mean = combinedMean; + this.m2 = combinedM2; + if (other.min < this.min) this.min = other.min; + if (other.max > this.max) this.max = other.max; + } + + public void reset() { + count = 0; + mean = 0.0; + m2 = 0.0; + min = Double.MAX_VALUE; + max = Double.MIN_VALUE; + } +} diff --git a/src/main/java/net/runelite/rsb/script/adaptive/stats/SuccessRateTracker.java b/src/main/java/net/runelite/rsb/script/adaptive/stats/SuccessRateTracker.java new file mode 100644 index 00000000..f5105e14 --- /dev/null +++ b/src/main/java/net/runelite/rsb/script/adaptive/stats/SuccessRateTracker.java @@ -0,0 +1,61 @@ +package net.runelite.rsb.script.adaptive.stats; + +/** + * Windowed circular buffer for tracking success rate over the last N actions. + */ +public class SuccessRateTracker { + private final boolean[] buffer; + private final int windowSize; + private int head; + private int count; + private int successes; + + public SuccessRateTracker(int windowSize) { + if (windowSize <= 0) { + throw new IllegalArgumentException("Window size must be positive: " + windowSize); + } + this.windowSize = windowSize; + this.buffer = new boolean[windowSize]; + this.head = 0; + this.count = 0; + this.successes = 0; + } + + public void record(boolean success) { + if (count >= windowSize) { + // Remove the oldest entry + if (buffer[head]) { + successes--; + } + } else { + count++; + } + buffer[head] = success; + if (success) { + successes++; + } + head = (head + 1) % windowSize; + } + + public double getSuccessRate() { + return count > 0 ? (double) successes / count : 0.0; + } + + public int getSuccessCount() { + return successes; + } + + public int getTotalCount() { + return count; + } + + public int getWindowSize() { + return windowSize; + } + + public void reset() { + head = 0; + count = 0; + successes = 0; + } +} diff --git a/src/main/java/net/runelite/rsb/service/FileScriptSource.java b/src/main/java/net/runelite/rsb/service/FileScriptSource.java index 051248d6..8eb98ea3 100644 --- a/src/main/java/net/runelite/rsb/service/FileScriptSource.java +++ b/src/main/java/net/runelite/rsb/service/FileScriptSource.java @@ -31,13 +31,17 @@ public List list() { if (file != null) { if (file.isDirectory()) { try { - ClassLoader scriptLoader = new ScriptClassLoader(file.toURI().toURL()); for (File file : Objects.requireNonNull(file.listFiles())) { - if (isJar(file)) { - load(new ScriptClassLoader(getJarUrl(file)), scriptDefinitions, new JarFile(file)); - } else { - load(scriptLoader, scriptDefinitions, file, ""); + try { + if (isJar(file)) { + load(new ScriptClassLoader(getJarUrl(file)), scriptDefinitions, new JarFile(file)); + } else { + load(scriptLoader, scriptDefinitions, file, ""); + } + } catch (Throwable t) { + log.warn("Failed to process script file {} — skipping ({}): {}", + file.getName(), t.getClass().getSimpleName(), t.getMessage()); } } } catch (IOException ioEx) { @@ -100,11 +104,8 @@ private void load(ClassLoader loader, LinkedList scripts, Stri Class clazz; try { clazz = loader.loadClass(name); - } catch (Exception ex) { - log.warn("Exception occurred " + name + " is not a valid script and was ignored!", ex); - return; - } catch (VerifyError verEx) { - log.warn("VerifyError exception occurred " + name + " is not a valid script and was ignored!", verEx); + } catch (Throwable t) { + log.warn("Failed to load class {} — skipping ({}): {}", name, t.getClass().getSimpleName(), t.getMessage()); return; } if (clazz.isAnnotationPresent(ScriptManifest.class)) { diff --git a/src/main/java/net/runelite/rsb/service/ScriptClassLoader.java b/src/main/java/net/runelite/rsb/service/ScriptClassLoader.java index c12a55c4..21faaf97 100644 --- a/src/main/java/net/runelite/rsb/service/ScriptClassLoader.java +++ b/src/main/java/net/runelite/rsb/service/ScriptClassLoader.java @@ -44,7 +44,7 @@ public Class loadClass(String name, boolean resolve) throws ClassNotFoundExce if (resolve) { resolveClass(clazz); } - } catch (Exception e) { + } catch (Throwable t) { clazz = super.loadClass(name, resolve); } } diff --git a/src/main/java/net/runelite/rsb/wrappers/RSNPC.java b/src/main/java/net/runelite/rsb/wrappers/RSNPC.java index 5bb36d20..b81c816b 100644 --- a/src/main/java/net/runelite/rsb/wrappers/RSNPC.java +++ b/src/main/java/net/runelite/rsb/wrappers/RSNPC.java @@ -17,17 +17,51 @@ import java.io.IOException; import java.lang.ref.SoftReference; import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; public class RSNPC extends RSCharacter implements CacheProvider { private static HashMap npcDefinitionCache; private static HashMap npcFileCache; + /** Static cache for definitions resolved via RuneLite client API. */ + private static final ConcurrentHashMap clientApiDefCache = new ConcurrentHashMap<>(); private final SoftReference npc; private final NpcDefinition def; public RSNPC(final MethodContext ctx, final NPC npc) { super(ctx); this.npc = new SoftReference<>(npc); - this.def = (npc.getId() != -1) ? (NpcDefinition) createDefinition(npc.getId()) : null; + this.def = resolveDefinition(npc); + } + + private NpcDefinition resolveDefinition(NPC npc) { + int npcId = npc.getId(); + if (npcId == -1) return null; + + // Try file-based cache first + NpcDefinition cached = (NpcDefinition) createDefinition(npcId); + if (cached != null) return cached; + + // Try static client API cache + NpcDefinition apiCached = clientApiDefCache.get(npcId); + if (apiCached != null) return apiCached; + + // Build from RuneLite NPC API + String name = npc.getName(); + if (name != null && !name.isEmpty()) { + NpcDefinition fallback = new NpcDefinition(npcId); + fallback.setName(name); + try { + NPCComposition comp = npc.getTransformedComposition(); + if (comp != null) { + fallback.setActions(comp.getActions()); + } + } catch (Exception e) { + // Composition not available + } + clientApiDefCache.put(npcId, fallback); + return fallback; + } + return null; } @Override diff --git a/src/main/java/net/runelite/rsb/wrappers/RSObject.java b/src/main/java/net/runelite/rsb/wrappers/RSObject.java index 7b1ffb29..c343d80f 100644 --- a/src/main/java/net/runelite/rsb/wrappers/RSObject.java +++ b/src/main/java/net/runelite/rsb/wrappers/RSObject.java @@ -18,6 +18,7 @@ import java.awt.*; import java.lang.reflect.Field; +import java.util.concurrent.ConcurrentHashMap; /** * A wrapper for a tile object which interprets the underlying tile objects type and furthermore @@ -28,6 +29,9 @@ @Slf4j public class RSObject extends MethodProvider implements Clickable07, Positionable, CacheProvider { + /** Static cache for definitions resolved via RuneLite client API. */ + private static final ConcurrentHashMap clientApiDefCache = new ConcurrentHashMap<>(); + private final TileObject obj; private final Type type; private final int plane; @@ -52,7 +56,41 @@ public RSObject(final MethodContext ctx, this.type = type; this.plane = plane; this.id = (obj != null) ? obj.getId() : -1; - this.def = (id != -1) ? (ObjectDefinition) createDefinition(id) : null; + this.def = resolveDefinition(id); + } + + /** + * Resolves the ObjectDefinition for the given ID, trying the file cache first, + * then falling back to RuneLite's client API with a static cache. + */ + private ObjectDefinition resolveDefinition(int id) { + if (id == -1) return null; + + // Try file-based cache first (original CacheProvider behavior) + ObjectDefinition cached = (ObjectDefinition) createDefinition(id); + if (cached != null) return cached; + + // Try static client API cache (avoids repeated lookups for same ID) + ObjectDefinition apiCached = clientApiDefCache.get(id); + if (apiCached != null) return apiCached; + + // Fall back to RuneLite client API + if (methods != null && methods.client != null) { + try { + ObjectComposition comp = methods.client.getObjectDefinition(id); + if (comp != null && comp.getName() != null && !"null".equals(comp.getName())) { + ObjectDefinition fallback = new ObjectDefinition(); + fallback.setId(id); + fallback.setName(comp.getName()); + fallback.setActions(comp.getActions()); + clientApiDefCache.put(id, fallback); + return fallback; + } + } catch (Exception e) { + // Client API unavailable + } + } + return null; } /** @@ -205,7 +243,9 @@ public Type getType() { * desired action */ public boolean hasAction(@NonNull final String action) { - for (final String a : getDef().getActions()) { + ObjectDefinition objectDef = getDef(); + if (objectDef == null) return false; + for (final String a : objectDef.getActions()) { if (action.equalsIgnoreCase(a)) return true; } return false; diff --git a/src/main/java/net/runelite/rsb/wrappers/client_wrapper/BaseClientWrapper.java b/src/main/java/net/runelite/rsb/wrappers/client_wrapper/BaseClientWrapper.java index 1b7d7890..829d74b4 100644 --- a/src/main/java/net/runelite/rsb/wrappers/client_wrapper/BaseClientWrapper.java +++ b/src/main/java/net/runelite/rsb/wrappers/client_wrapper/BaseClientWrapper.java @@ -12,33 +12,31 @@ import net.runelite.api.vars.AccountType; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; +import net.runelite.api.widgets.WidgetConfigNode; import net.runelite.api.widgets.WidgetModalMode; import net.runelite.api.worldmap.MapElementConfig; import net.runelite.api.worldmap.WorldMap; -import net.runelite.api.RuneLiteObjectController; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.applet.Applet; import java.awt.*; import java.lang.reflect.Method; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.function.IntPredicate; -import com.jagex.oldscape.pub.OAuthApi; -import com.jagex.oldscape.pub.OtlTokenRequester; -import com.jagex.oldscape.pub.OtlTokenResponse; /* -Base class for wrapping runelite Client, along with some weird Applet shenanigans. +Base class for wrapping runelite Client. +In RuneLite 1.12+ the client no longer extends Applet, so we extend Panel +(still a Component/Container) to preserve AWT event compatibility. */ @SuppressWarnings("removal") -public abstract class BaseClientWrapper extends Applet implements Client, OAuthApi +public abstract class BaseClientWrapper extends Panel implements Client { public final Client wrappedClient; @@ -48,7 +46,9 @@ public BaseClientWrapper(Client client) { @Override public Component getComponent(int n) { - return ((Applet) wrappedClient).getComponent(n); + // In 1.12+ the client is not an Applet/Container. + // Return the game canvas for component 0 (the only child in the old model). + return wrappedClient.getCanvas(); } @Override @@ -81,15 +81,7 @@ public List getNpcs() { return wrappedClient.getNpcs(); } - @Override - public NPC[] getCachedNPCs() { - return wrappedClient.getCachedNPCs(); - } - - @Override - public Player[] getCachedPlayers() { - return wrappedClient.getCachedPlayers(); - } + // getCachedNPCs() and getCachedPlayers() removed from RuneLite Client API @Override public int getBoostedSkillLevel(Skill skill) { @@ -656,7 +648,7 @@ public WidgetNode openInterface(int componentId, int interfaceId, int modalNode) public void closeInterface(WidgetNode interfaceNode, boolean unload) { wrappedClient.closeInterface(interfaceNode, unload); } @Override - public HashTable getWidgetFlags() { + public HashTable getWidgetFlags() { return wrappedClient.getWidgetFlags(); } @@ -789,6 +781,11 @@ public Projectile createProjectile(int id, int plane, int startX, int startY, in return wrappedClient.createProjectile(id, plane, startX, startY, startZ, startCycle, endCycle, slope, startHeight, endHeight, target, targetX, targetY); } + @Override + public Projectile createProjectile(int id, WorldPoint start, int startZ, @Nullable Actor startActor, WorldPoint end, int endZ, @Nullable Actor endActor, int startCycle, int endCycle, int slope, int startHeight) { + return wrappedClient.createProjectile(id, start, startZ, startActor, end, endZ, endActor, startCycle, endCycle, slope, startHeight); + } + @Override public Deque getProjectiles() { return wrappedClient.getProjectiles(); @@ -820,6 +817,16 @@ public ModelData mergeModels(ModelData... models) { return wrappedClient.mergeModels(models); } + @Override + public Model mergeModels(Model[] models, int length) { + return wrappedClient.mergeModels(models, length); + } + + @Override + public Model mergeModels(Model... models) { + return wrappedClient.mergeModels(models); + } + @Override @Nullable public Model loadModel(int id) { @@ -925,26 +932,13 @@ public int[] getIntStack() { return wrappedClient.getIntStack(); } - @Override - public int getStringStackSize() { - return wrappedClient.getStringStackSize(); - } - - @Override - public void setStringStackSize(int stackSize) { - wrappedClient.setStringStackSize(stackSize); - } + // getStringStackSize/setStringStackSize/getStringStack removed from RuneLite Client API @Override public void setCameraPitchTarget(int cameraPitchTarget) { wrappedClient.setCameraPitchTarget(cameraPitchTarget); } - @Override - public String[] getStringStack() { - return wrappedClient.getStringStack(); - } - @Override public Widget getScriptActiveWidget() { return wrappedClient.getScriptActiveWidget(); @@ -1644,18 +1638,99 @@ public int getDraw2DMask() { return wrappedClient.getDraw2DMask(); } + @Override + public java.io.FileDescriptor getSocketFD() { + return wrappedClient.getSocketFD(); + } + + @Override + public WorldView findWorldViewFromWorldPoint(WorldPoint point) { + return wrappedClient.findWorldViewFromWorldPoint(point); + } + + @Override + @Nullable + public CameraFocusableEntity getCameraFocusEntity() { + return wrappedClient.getCameraFocusEntity(); + } + + @Override + public SceneTilePaint createSceneTilePaint(int swColor, int seColor, int neColor, int nwColor, int texture, int rgb, boolean isFlat) { + return wrappedClient.createSceneTilePaint(swColor, seColor, neColor, nwColor, texture, rgb, isFlat); + } + + @Override + public int getEnvironment() { + return wrappedClient.getEnvironment(); + } + + @Override + public Widget getFocusedInputFieldWidget() { + return wrappedClient.getFocusedInputFieldWidget(); + } + + @Override + public WidgetConfigNode getWidgetConfig(Widget widget) { + return wrappedClient.getWidgetConfig(widget); + } + + @Override + public List getDBTableRows(int tableId) { + return wrappedClient.getDBTableRows(tableId); + } + + @Override + public void registerRuneLiteObject(RuneLiteObjectController obj) { + wrappedClient.registerRuneLiteObject(obj); + } + + @Override + public void removeRuneLiteObject(RuneLiteObjectController obj) { + wrappedClient.removeRuneLiteObject(obj); + } + + @Override + public boolean isRuneLiteObjectRegistered(RuneLiteObjectController obj) { + return wrappedClient.isRuneLiteObjectRegistered(obj); + } + @Override public List getActiveMidiRequests() { return wrappedClient.getActiveMidiRequests(); } @Override - public boolean isRuneLiteObjectRegistered(RuneLiteObjectController controller) {return wrappedClient.isRuneLiteObjectRegistered(controller);} + public int getObjectStackSize() { + return wrappedClient.getObjectStackSize(); + } + + @Override + public void setObjectStackSize(int size) { + wrappedClient.setObjectStackSize(size); + } + + @Override + public Object[] getObjectStack() { + return wrappedClient.getObjectStack(); + } + + @Override + public String getWorldHost() { + return wrappedClient.getWorldHost(); + } @Override - public void removeRuneLiteObject(RuneLiteObjectController controller) {wrappedClient.removeRuneLiteObject(controller);} - + public void unblockStartup() { + wrappedClient.unblockStartup(); + } + @Override - public void registerRuneLiteObject(RuneLiteObjectController controller) {wrappedClient.registerRuneLiteObject(controller);} - + public void setConfiguration(ClientConfiguration config) { + wrappedClient.setConfiguration(config); + } + + @Override + public void initialize() { + wrappedClient.initialize(); + } } diff --git a/src/main/java/net/runelite/rsb/wrappers/client_wrapper/BaseWidgetWrapper.java b/src/main/java/net/runelite/rsb/wrappers/client_wrapper/BaseWidgetWrapper.java index 2bd8691d..a47d87fa 100644 --- a/src/main/java/net/runelite/rsb/wrappers/client_wrapper/BaseWidgetWrapper.java +++ b/src/main/java/net/runelite/rsb/wrappers/client_wrapper/BaseWidgetWrapper.java @@ -257,7 +257,13 @@ public Widget setSpriteId(int spriteId) { @Override public boolean isHidden() { - return wrappedWidget.isHidden(); + try { + return wrappedWidget.isHidden(); + } catch (IllegalStateException e) { + // isHidden() requires client thread in RuneLite 1.12+ (traverses parent widgets). + // Fall back to isSelfHidden() which only reads this widget's own flag. + return wrappedWidget.isSelfHidden(); + } } @Override diff --git a/src/main/java/net/runelite/rsb/wrappers/client_wrapper/RSClient.java b/src/main/java/net/runelite/rsb/wrappers/client_wrapper/RSClient.java index a32d3fbb..b6b2eecd 100644 --- a/src/main/java/net/runelite/rsb/wrappers/client_wrapper/RSClient.java +++ b/src/main/java/net/runelite/rsb/wrappers/client_wrapper/RSClient.java @@ -7,11 +7,6 @@ import net.runelite.api.widgets.WidgetInfo; import net.runelite.api.worldmap.MapElementConfig; import net.runelite.client.callback.ClientThread; -import net.runelite.api.RuneLiteObjectController; -import com.jagex.oldscape.pub.OAuthApi; -import com.jagex.oldscape.pub.OtlTokenRequester; -import com.jagex.oldscape.pub.OtlTokenResponse; -import com.jagex.oldscape.pub.RefreshAccessTokenRequester; import javax.annotation.Nullable; import java.time.Instant; import java.util.*; @@ -319,6 +314,36 @@ public void clearActions() { public int[] getVarTransmitTrigger() { return super.getVarTransmitTrigger(); } + + @Override + public int getTargetPriority() { + return wrappedWidget.getTargetPriority(); + } + + @Override + public void setTargetPriority(int priority) { + wrappedWidget.setTargetPriority(priority); + } + + @Override + public boolean isFlippedHorizontally() { + return wrappedWidget.isFlippedHorizontally(); + } + + @Override + public void setFlippedHorizontally(boolean flipped) { + wrappedWidget.setFlippedHorizontally(flipped); + } + + @Override + public boolean isFlippedVertically() { + return wrappedWidget.isFlippedVertically(); + } + + @Override + public void setFlippedVertically(boolean flipped) { + wrappedWidget.setFlippedVertically(flipped); + } } @@ -627,30 +652,6 @@ public int getDraw2DMask() { return super.getDraw2DMask(); } - @Override - public List getActiveMidiRequests() { - return convertResult(super.getActiveMidiRequests()); - } - - @Override - public boolean isRuneLiteObjectRegistered(RuneLiteObjectController controller) { - return super.isRuneLiteObjectRegistered(controller); - } - - @Override - public void removeRuneLiteObject(RuneLiteObjectController controller) { - super.removeRuneLiteObject(controller); - } - - @Override - public void registerRuneLiteObject(RuneLiteObjectController controller) { - super.registerRuneLiteObject(controller); - } - - public void px(OtlTokenRequester requester) { - px(requester); - } - public long qx() { return qx(); } @@ -667,27 +668,6 @@ public boolean ps() { return ps(); } - public void setOtlTokenRequester(OtlTokenRequester requester) { - setOtlTokenRequester(requester); - } - - public void pr(RefreshAccessTokenRequester requester) { - pr(requester); - } - - public void pn(OtlTokenRequester requester) { - pn(requester); - } - - public void pm(RefreshAccessTokenRequester requester) { - pm(requester); - } - - public void setRefreshTokenRequester(RefreshAccessTokenRequester requester) { - setRefreshTokenRequester(requester); - } - - public void setClient(int client) { setClient(client); } @@ -696,6 +676,11 @@ public void setClient(int client) { public boolean isOnLoginScreen() { return isOnLoginScreen(); } -public boolean pf() { return pf(); } -public void pg(int a) { pg(a); } + public boolean pf() { return pf(); } + public void pg(int a) { pg(a); } + + @Override + public java.io.FileDescriptor getSocketFD() { + return wrappedClient.getSocketFD(); + } } diff --git a/src/main/resources/net/runelite/client/plugins/bot/open-folder.png b/src/main/resources/net/runelite/client/plugins/bot/open-folder.png new file mode 100644 index 00000000..e06a7dc6 Binary files /dev/null and b/src/main/resources/net/runelite/client/plugins/bot/open-folder.png differ diff --git a/src/main/resources/net/runelite/client/plugins/bot/pause.png b/src/main/resources/net/runelite/client/plugins/bot/pause.png new file mode 100644 index 00000000..399cc49b Binary files /dev/null and b/src/main/resources/net/runelite/client/plugins/bot/pause.png differ diff --git a/src/main/resources/net/runelite/client/plugins/bot/reload.png b/src/main/resources/net/runelite/client/plugins/bot/reload.png new file mode 100644 index 00000000..bed0db84 Binary files /dev/null and b/src/main/resources/net/runelite/client/plugins/bot/reload.png differ diff --git a/src/main/resources/net/runelite/client/plugins/bot/rsb.png b/src/main/resources/net/runelite/client/plugins/bot/rsb.png new file mode 100644 index 00000000..c9e82711 Binary files /dev/null and b/src/main/resources/net/runelite/client/plugins/bot/rsb.png differ diff --git a/src/main/resources/net/runelite/client/plugins/bot/start.png b/src/main/resources/net/runelite/client/plugins/bot/start.png new file mode 100644 index 00000000..00391aed Binary files /dev/null and b/src/main/resources/net/runelite/client/plugins/bot/start.png differ diff --git a/src/main/resources/net/runelite/client/plugins/bot/stop.png b/src/main/resources/net/runelite/client/plugins/bot/stop.png new file mode 100644 index 00000000..5b03ad0f Binary files /dev/null and b/src/main/resources/net/runelite/client/plugins/bot/stop.png differ