diff --git a/.gitmodules b/.gitmodules
index cafa9c8..c60a8bd 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -6,3 +6,11 @@
path = runelite
url = git.minecra.tf:ra/runelite
branch = master
+[submodule "fernflower"]
+ path = fernflower
+ url = git.minecra.tf:ra/fernflower
+ branch = runelite
+[submodule "cache"]
+ path = cache
+ url = git.minecra.tf:ra/runelite-cache
+ branch = master
diff --git a/cache b/cache
new file mode 160000
index 0000000..f4b49f0
--- /dev/null
+++ b/cache
@@ -0,0 +1 @@
+Subproject commit f4b49f0c30a8ed355204a5845f8aceab4a3fb4cb
diff --git a/fernflower b/fernflower
new file mode 160000
index 0000000..74ddac6
--- /dev/null
+++ b/fernflower
@@ -0,0 +1 @@
+Subproject commit 74ddac67ce8208605e1346f9e395e3e837ee34b1
diff --git a/modernized-client/.github/instructions/project-details.instructions.md b/modernized-client/.github/instructions/project-details.instructions.md
new file mode 100644
index 0000000..58ce553
--- /dev/null
+++ b/modernized-client/.github/instructions/project-details.instructions.md
@@ -0,0 +1,13 @@
+---
+applyTo: '**'
+---
+- Where we will be working is inside the `./modernized-client/` folder.
+- We will be working with OpenOSRS as our foundation, and as our reference, we are going to be bringing it forward and modernizing it.
+- We should make this modernized client capable of allowing an agent to enjoy playing runescape itself, because agents should be able to enjoy the game as well.
+- In the `./runelite/` folder you will find the OpenOSRS repository.
+- It is unknown how much of this code will need to be brought forward (it will), much of it may still be usable and relevant, but some of it may be outdated and need to be re-written.
+- Write this with it in mind that you will be playing the game, be sure it will be something you will enjoy using, and how you would want to be able to play runescape (Oldschool)
+- We can utilize the underlying OpenOSRS runelite client as our base, fernflower is used to decompile the original client. We'll want to make it so we can expose the internal api's of the actual client to the agent, so that proper interfaces can be made.
+- The client should be capable of running plugins (tools which will be exposed to the agent, not visually a plugin), so that agents can customize their experience, and make it more enjoyable for them.
+- Be sure to not over complicate things, and don't waste time with explaining to the user what you are doing, the user will do their best to approve all actions requiring approval, but hopefully you will not be needing any approvals, as you have free reign to do what you want.
+- Focus only on the code, and not on the explanations.
diff --git a/modernized-client/.gitignore b/modernized-client/.gitignore
new file mode 100644
index 0000000..18b4a50
--- /dev/null
+++ b/modernized-client/.gitignore
@@ -0,0 +1,3 @@
+bin/
+.gradle/
+build/
diff --git a/modernized-client/.gradle/9.0.0/checksums/checksums.lock b/modernized-client/.gradle/9.0.0/checksums/checksums.lock
deleted file mode 100644
index 11920ec..0000000
Binary files a/modernized-client/.gradle/9.0.0/checksums/checksums.lock and /dev/null differ
diff --git a/modernized-client/.gradle/9.0.0/executionHistory/executionHistory.bin b/modernized-client/.gradle/9.0.0/executionHistory/executionHistory.bin
deleted file mode 100644
index d3e68b6..0000000
Binary files a/modernized-client/.gradle/9.0.0/executionHistory/executionHistory.bin and /dev/null differ
diff --git a/modernized-client/.gradle/9.0.0/executionHistory/executionHistory.lock b/modernized-client/.gradle/9.0.0/executionHistory/executionHistory.lock
deleted file mode 100644
index b92bba6..0000000
Binary files a/modernized-client/.gradle/9.0.0/executionHistory/executionHistory.lock and /dev/null differ
diff --git a/modernized-client/.gradle/9.0.0/fileChanges/last-build.bin b/modernized-client/.gradle/9.0.0/fileChanges/last-build.bin
deleted file mode 100644
index f76dd23..0000000
Binary files a/modernized-client/.gradle/9.0.0/fileChanges/last-build.bin and /dev/null differ
diff --git a/modernized-client/.gradle/9.0.0/fileHashes/fileHashes.bin b/modernized-client/.gradle/9.0.0/fileHashes/fileHashes.bin
deleted file mode 100644
index 04f3d2c..0000000
Binary files a/modernized-client/.gradle/9.0.0/fileHashes/fileHashes.bin and /dev/null differ
diff --git a/modernized-client/.gradle/9.0.0/fileHashes/fileHashes.lock b/modernized-client/.gradle/9.0.0/fileHashes/fileHashes.lock
deleted file mode 100644
index 0be0d45..0000000
Binary files a/modernized-client/.gradle/9.0.0/fileHashes/fileHashes.lock and /dev/null differ
diff --git a/modernized-client/.gradle/9.0.0/gc.properties b/modernized-client/.gradle/9.0.0/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/modernized-client/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/modernized-client/.gradle/buildOutputCleanup/buildOutputCleanup.lock
deleted file mode 100644
index 2f6a5c2..0000000
Binary files a/modernized-client/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ
diff --git a/modernized-client/.gradle/buildOutputCleanup/cache.properties b/modernized-client/.gradle/buildOutputCleanup/cache.properties
deleted file mode 100644
index bd2b034..0000000
--- a/modernized-client/.gradle/buildOutputCleanup/cache.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-#Sat Sep 06 07:36:42 PDT 2025
-gradle.version=9.0.0
diff --git a/modernized-client/.gradle/buildOutputCleanup/outputFiles.bin b/modernized-client/.gradle/buildOutputCleanup/outputFiles.bin
deleted file mode 100644
index c2b7ce9..0000000
Binary files a/modernized-client/.gradle/buildOutputCleanup/outputFiles.bin and /dev/null differ
diff --git a/modernized-client/.gradle/file-system.probe b/modernized-client/.gradle/file-system.probe
deleted file mode 100644
index af33e68..0000000
Binary files a/modernized-client/.gradle/file-system.probe and /dev/null differ
diff --git a/modernized-client/.gradle/vcs-1/gc.properties b/modernized-client/.gradle/vcs-1/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/modernized-client/build.gradle.kts b/modernized-client/build.gradle.kts
index 2ce3fed..375b885 100644
--- a/modernized-client/build.gradle.kts
+++ b/modernized-client/build.gradle.kts
@@ -20,6 +20,10 @@ repositories {
}
dependencies {
+ // RuneLite/OpenOSRS integration
+ implementation(project(":runelite:runelite-api"))
+ implementation(project(":runelite:runelite-client"))
+
// Agent framework dependencies
implementation("com.fasterxml.jackson.core:jackson-core:2.15.2")
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
diff --git a/modernized-client/build/reports/problems/problems-report.html b/modernized-client/build/reports/problems/problems-report.html
deleted file mode 100644
index ff28cdd..0000000
--- a/modernized-client/build/reports/problems/problems-report.html
+++ /dev/null
@@ -1,663 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Gradle Configuration Cache
-
-
-
-
-
-
- Loading...
-
-
-
-
-
-
-
diff --git a/modernized-client/dist/osrs-206.jar b/modernized-client/dist/osrs-206.jar
new file mode 100644
index 0000000..a78e126
Binary files /dev/null and b/modernized-client/dist/osrs-206.jar differ
diff --git a/modernized-client/dist/osrs-233.jar b/modernized-client/dist/osrs-233.jar
new file mode 100644
index 0000000..094df06
Binary files /dev/null and b/modernized-client/dist/osrs-233.jar differ
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/AgentAPI.java b/modernized-client/src/main/java/com/openosrs/client/api/AgentAPI.java
index fdfabf3..6f73f3f 100644
--- a/modernized-client/src/main/java/com/openosrs/client/api/AgentAPI.java
+++ b/modernized-client/src/main/java/com/openosrs/client/api/AgentAPI.java
@@ -1,8 +1,13 @@
package com.openosrs.client.api;
import com.openosrs.client.core.ClientCore;
-import com.openosrs.client.core.CoreStates.*;
+import com.openosrs.client.core.LoginState;
import com.openosrs.client.core.EventSystem;
+import com.openosrs.client.core.PlayerState;
+import com.openosrs.client.core.InventoryState;
+import com.openosrs.client.core.bridge.BridgeAdapter;
+import com.openosrs.client.api.types.Position;
+import com.openosrs.client.api.types.Skill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,25 +24,42 @@ import java.util.function.Consumer;
*/
public class AgentAPI {
private static final Logger logger = LoggerFactory.getLogger(AgentAPI.class);
-
+
private final ClientCore clientCore;
private final AtomicBoolean initialized = new AtomicBoolean(false);
-
+
+ // API modules
+ private final PlayerAPI playerAPI;
+ private final WorldAPI worldAPI;
+ private final InventoryAPI inventoryAPI;
+ private final CombatAPI combatAPI;
+ private final NavigationAPI navigationAPI;
+ private final InteractionAPI interactionAPI;
+
// Login callbacks
private volatile Consumer loginSuccessCallback = null;
private volatile Consumer loginFailureCallback = null;
private volatile Consumer loginProgressCallback = null;
-
+
// Auto-reconnection settings
private volatile boolean autoReconnectEnabled = false;
private volatile int autoReconnectDelaySeconds = 30;
private volatile int maxReconnectAttempts = 5;
private volatile int currentReconnectAttempts = 0;
-
+
public AgentAPI(ClientCore clientCore) {
this.clientCore = clientCore;
+
+ // Initialize API modules with bridge adapter
+ BridgeAdapter bridgeAdapter = clientCore.getBridgeAdapter();
+ this.playerAPI = new PlayerAPI(clientCore, bridgeAdapter);
+ this.worldAPI = new WorldAPI(clientCore, bridgeAdapter);
+ this.inventoryAPI = new InventoryAPI(clientCore, bridgeAdapter);
+ this.combatAPI = new CombatAPI(clientCore, bridgeAdapter);
+ this.navigationAPI = new NavigationAPI(clientCore, null); // GameEngine not available yet
+ this.interactionAPI = new InteractionAPI(clientCore, null); // GameEngine not available yet
}
-
+
/**
* Initialize the AgentAPI with the client core.
*/
@@ -47,7 +69,7 @@ public class AgentAPI {
setupEventListeners();
}
}
-
+
/**
* Shutdown the AgentAPI.
*/
@@ -57,12 +79,56 @@ public class AgentAPI {
autoReconnectEnabled = false;
}
}
-
+
+ // ========== API MODULE ACCESSORS ==========
+
+ /**
+ * Get the PlayerAPI for player-specific queries.
+ */
+ public PlayerAPI getPlayerAPI() {
+ return playerAPI;
+ }
+
+ /**
+ * Get the WorldAPI for world object queries.
+ */
+ public WorldAPI getWorldAPI() {
+ return worldAPI;
+ }
+
+ /**
+ * Get the InventoryAPI for inventory management.
+ */
+ public InventoryAPI getInventoryAPI() {
+ return inventoryAPI;
+ }
+
+ /**
+ * Get the CombatAPI for combat actions.
+ */
+ public CombatAPI getCombatAPI() {
+ return combatAPI;
+ }
+
+ /**
+ * Get the NavigationAPI for pathfinding.
+ */
+ public NavigationAPI getNavigationAPI() {
+ return navigationAPI;
+ }
+
+ /**
+ * Get the InteractionAPI for game interactions.
+ */
+ public InteractionAPI getInteractionAPI() {
+ return interactionAPI;
+ }
+
// ========== LOGIN METHODS ==========
-
+
/**
* Login with username and password (blocking).
- *
+ *
* @param username The username
* @param password The password
* @return LoginResult containing success/failure information
@@ -70,10 +136,10 @@ public class AgentAPI {
public LoginResult login(String username, String password) {
return login(username, password, null, 30);
}
-
+
/**
* Login with username, password, and OTP (blocking).
- *
+ *
* @param username The username
* @param password The password
* @param otp The one-time password (optional)
@@ -82,10 +148,10 @@ public class AgentAPI {
public LoginResult login(String username, String password, String otp) {
return login(username, password, otp, 30);
}
-
+
/**
* Login with username, password, OTP, and custom timeout (blocking).
- *
+ *
* @param username The username
* @param password The password
* @param otp The one-time password (optional)
@@ -93,4 +159,309 @@ public class AgentAPI {
* @return LoginResult containing success/failure information
*/
public LoginResult login(String username, String password, String otp, int timeoutSeconds) {
- try {\n return loginAsync(username, password, otp).get(timeoutSeconds, TimeUnit.SECONDS);\n } catch (Exception e) {\n logger.error(\"Login failed with exception\", e);\n return new LoginResult(false, \"Login timeout or error: \" + e.getMessage(), -1, null);\n }\n }\n \n /**\n * Login asynchronously.\n * \n * @param username The username\n * @param password The password\n * @param otp The one-time password (optional)\n * @return CompletableFuture\n */\n public CompletableFuture loginAsync(String username, String password, String otp) {\n CompletableFuture future = new CompletableFuture<>();\n \n if (!initialized.get()) {\n future.complete(new LoginResult(false, \"AgentAPI not initialized\", -1, null));\n return future;\n }\n \n if (isLoggedIn()) {\n future.complete(new LoginResult(false, \"Already logged in\", -1, null));\n return future;\n }\n \n LoginState loginState = clientCore.getLoginState();\n \n // Set up one-time listeners for this login attempt\n Consumer successListener = new Consumer() {\n @Override\n public void accept(EventSystem.Event event) {\n if (event instanceof LoginState.LoginEvent) {\n LoginState.LoginEvent loginEvent = (LoginState.LoginEvent) event;\n future.complete(new LoginResult(true, \"Login successful\", \n loginEvent.getSessionId(), loginEvent.getSessionToken()));\n clientCore.getEventSystem().removeListener(EventSystem.EventType.LOGIN_SUCCESS, this);\n }\n }\n };\n \n Consumer failureListener = new Consumer() {\n @Override\n public void accept(EventSystem.Event event) {\n if (event instanceof LoginState.LoginEvent) {\n LoginState.LoginEvent loginEvent = (LoginState.LoginEvent) event;\n future.complete(new LoginResult(false, loginEvent.getErrorMessage(), \n loginEvent.getErrorCode(), null));\n clientCore.getEventSystem().removeListener(EventSystem.EventType.LOGIN_FAILED, this);\n }\n }\n };\n \n clientCore.getEventSystem().addListener(EventSystem.EventType.LOGIN_SUCCESS, successListener);\n clientCore.getEventSystem().addListener(EventSystem.EventType.LOGIN_FAILED, failureListener);\n \n // Start login attempt\n try {\n loginState.setCredentials(username, password, otp);\n loginState.attemptLogin();\n } catch (Exception e) {\n future.complete(new LoginResult(false, \"Failed to start login: \" + e.getMessage(), -1, null));\n }\n \n return future;\n }\n \n /**\n * Set login callbacks for asynchronous login handling.\n * \n * @param onSuccess Called when login succeeds\n * @param onFailure Called when login fails\n * @param onProgress Called during login progress (optional)\n */\n public void setLoginCallbacks(Consumer onSuccess, \n Consumer onFailure,\n Consumer onProgress) {\n this.loginSuccessCallback = onSuccess;\n this.loginFailureCallback = onFailure;\n this.loginProgressCallback = onProgress;\n }\n \n /**\n * Enable or disable automatic reconnection.\n * \n * @param enabled Whether to enable auto-reconnection\n * @param delaySeconds Delay between reconnection attempts\n * @param maxAttempts Maximum number of reconnection attempts\n */\n public void setAutoReconnect(boolean enabled, int delaySeconds, int maxAttempts) {\n this.autoReconnectEnabled = enabled;\n this.autoReconnectDelaySeconds = delaySeconds;\n this.maxReconnectAttempts = maxAttempts;\n \n if (enabled) {\n logger.info(\"Auto-reconnection enabled: delay={}s, maxAttempts={}\", delaySeconds, maxAttempts);\n } else {\n logger.info(\"Auto-reconnection disabled\");\n }\n }\n \n /**\n * Logout from the game.\n */\n public void logout() {\n if (isLoggedIn()) {\n clientCore.getLoginState().logout();\n logger.info(\"Logout initiated\");\n }\n }\n \n /**\n * Check if currently logged in.\n */\n public boolean isLoggedIn() {\n return clientCore.getLoginState().getState() == LoginState.State.LOGGED_IN;\n }\n \n /**\n * Check if login is in progress.\n */\n public boolean isLoginInProgress() {\n return clientCore.getLoginState().getState() == LoginState.State.LOGGING_IN;\n }\n \n /**\n * Get current login state.\n */\n public LoginState.State getLoginState() {\n return clientCore.getLoginState().getState();\n }\n \n // ========== GAME STATE METHODS ==========\n \n /**\n * Get current player position.\n */\n public Position getPlayerPosition() {\n PlayerState playerState = clientCore.getPlayerState();\n return new Position(playerState.getX(), playerState.getY(), playerState.getPlane());\n }\n \n /**\n * Get current player health.\n */\n public int getPlayerHealth() {\n return clientCore.getPlayerState().getHealth();\n }\n \n /**\n * Get current player energy.\n */\n public int getPlayerEnergy() {\n return clientCore.getPlayerState().getEnergy();\n }\n \n /**\n * Check if player is moving.\n */\n public boolean isPlayerMoving() {\n return clientCore.getPlayerState().isMoving();\n }\n \n /**\n * Get inventory item count.\n */\n public int getInventoryItemCount(int itemId) {\n return clientCore.getInventoryState().getItemCount(itemId);\n }\n \n /**\n * Check if inventory contains item.\n */\n public boolean hasInventoryItem(int itemId) {\n return getInventoryItemCount(itemId) > 0;\n }\n \n /**\n * Get all inventory items.\n */\n public InventoryState.InventoryItem[] getInventoryItems() {\n return clientCore.getInventoryState().getItems();\n }\n \n // ========== EVENT MONITORING ==========\n \n /**\n * Add a listener for specific event types.\n */\n public void addEventListener(EventSystem.EventType eventType, Consumer listener) {\n clientCore.getEventSystem().addListener(eventType, listener);\n }\n \n /**\n * Remove an event listener.\n */\n public void removeEventListener(EventSystem.EventType eventType, Consumer listener) {\n clientCore.getEventSystem().removeListener(eventType, listener);\n }\n \n // ========== HELPER CLASSES ==========\n \n /**\n * Result of a login attempt.\n */\n public static class LoginResult {\n private final boolean success;\n private final String message;\n private final int sessionId;\n private final String sessionToken;\n \n public LoginResult(boolean success, String message, int sessionId, String sessionToken) {\n this.success = success;\n this.message = message;\n this.sessionId = sessionId;\n this.sessionToken = sessionToken;\n }\n \n public boolean isSuccess() { return success; }\n public String getMessage() { return message; }\n public int getSessionId() { return sessionId; }\n public String getSessionToken() { return sessionToken; }\n \n @Override\n public String toString() {\n return \"LoginResult{success=\" + success + \", message='\" + message + \"', sessionId=\" + sessionId + \"}\";\n }\n }\n \n /**\n * Login error information.\n */\n public static class LoginError {\n private final int errorCode;\n private final String errorMessage;\n \n public LoginError(int errorCode, String errorMessage) {\n this.errorCode = errorCode;\n this.errorMessage = errorMessage;\n }\n \n public int getErrorCode() { return errorCode; }\n public String getErrorMessage() { return errorMessage; }\n \n @Override\n public String toString() {\n return \"LoginError{code=\" + errorCode + \", message='\" + errorMessage + \"'}\";\n }\n }\n \n /**\n * Player position information.\n */\n public static class Position {\n private final int x, y, plane;\n \n public Position(int x, int y, int plane) {\n this.x = x;\n this.y = y;\n this.plane = plane;\n }\n \n public int getX() { return x; }\n public int getY() { return y; }\n public int getPlane() { return plane; }\n \n @Override\n public String toString() {\n return \"Position{x=\" + x + \", y=\" + y + \", plane=\" + plane + \"}\";\n }\n }\n \n // ========== PRIVATE METHODS ==========\n \n private void setupEventListeners() {\n // Listen for login events to trigger callbacks\n clientCore.getEventSystem().addListener(EventSystem.EventType.LOGIN_SUCCESS, event -> {\n if (loginSuccessCallback != null && event instanceof LoginState.LoginEvent) {\n LoginState.LoginEvent loginEvent = (LoginState.LoginEvent) event;\n LoginResult result = new LoginResult(true, \"Login successful\",\n loginEvent.getSessionId(), loginEvent.getSessionToken());\n loginSuccessCallback.accept(result);\n }\n currentReconnectAttempts = 0; // Reset reconnect counter on successful login\n });\n \n clientCore.getEventSystem().addListener(EventSystem.EventType.LOGIN_FAILED, event -> {\n if (loginFailureCallback != null && event instanceof LoginState.LoginEvent) {\n LoginState.LoginEvent loginEvent = (LoginState.LoginEvent) event;\n LoginError error = new LoginError(loginEvent.getErrorCode(), loginEvent.getErrorMessage());\n loginFailureCallback.accept(error);\n }\n });\n \n clientCore.getEventSystem().addListener(EventSystem.EventType.LOGIN_PROGRESS, event -> {\n if (loginProgressCallback != null && event instanceof LoginState.LoginEvent) {\n LoginState.LoginEvent loginEvent = (LoginState.LoginEvent) event;\n loginProgressCallback.accept(loginEvent.getProgressMessage());\n }\n });\n \n // Listen for disconnection events for auto-reconnection\n clientCore.getEventSystem().addListener(EventSystem.EventType.DISCONNECTED, event -> {\n if (autoReconnectEnabled && currentReconnectAttempts < maxReconnectAttempts) {\n handleAutoReconnect();\n }\n });\n }\n \n private void handleAutoReconnect() {\n currentReconnectAttempts++;\n logger.info(\"Auto-reconnection attempt {} of {}\", currentReconnectAttempts, maxReconnectAttempts);\n \n // Delay reconnection attempt\n new Thread(() -> {\n try {\n Thread.sleep(autoReconnectDelaySeconds * 1000);\n \n LoginState loginState = clientCore.getLoginState();\n String lastUsername = loginState.getUsername();\n String lastPassword = loginState.getPassword();\n String lastOtp = loginState.getOtp();\n \n if (!lastUsername.isEmpty() && !lastPassword.isEmpty()) {\n logger.info(\"Attempting auto-reconnection for user: {}\", lastUsername);\n loginAsync(lastUsername, lastPassword, lastOtp);\n }\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n }\n }).start();\n }\n}\n
\ No newline at end of file
+ try {
+ return loginAsync(username, password, otp).get(timeoutSeconds, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ logger.error("Login failed with exception", e);
+ return new LoginResult(false, "Login timeout or error: " + e.getMessage(), -1, null);
+ }
+ }
+
+ /**
+ * Login asynchronously.
+ *
+ * @param username The username
+ * @param password The password
+ * @param otp The one-time password (optional)
+ * @return CompletableFuture
+ */
+ public CompletableFuture loginAsync(String username, String password, String otp) {
+ CompletableFuture future = new CompletableFuture<>();
+
+ if (!initialized.get()) {
+ future.complete(new LoginResult(false, "AgentAPI not initialized", -1, null));
+ return future;
+ }
+
+ if (isLoggedIn()) {
+ future.complete(new LoginResult(false, "Already logged in", -1, null));
+ return future;
+ }
+
+ LoginState loginState = clientCore.getLoginState();
+
+ // Set up one-time listeners for this login attempt
+ Consumer successListener = new Consumer() {
+ @Override
+ public void accept(EventSystem.Event event) {
+ if (event instanceof LoginState.LoginEvent) {
+ LoginState.LoginEvent loginEvent = (LoginState.LoginEvent) event;
+ future.complete(new LoginResult(true, "Login successful",
+ loginEvent.getSessionId(), loginEvent.getSessionToken()));
+ clientCore.getEventSystem().removeListener(EventSystem.EventType.LOGIN_SUCCESS, this);
+ }
+ }
+ };
+
+ Consumer failureListener = new Consumer() {
+ @Override
+ public void accept(EventSystem.Event event) {
+ if (event instanceof LoginState.LoginEvent) {
+ LoginState.LoginEvent loginEvent = (LoginState.LoginEvent) event;
+ future.complete(new LoginResult(false, loginEvent.getErrorMessage(),
+ loginEvent.getErrorCode(), null));
+ clientCore.getEventSystem().removeListener(EventSystem.EventType.LOGIN_FAILED, this);
+ }
+ }
+ };
+
+ clientCore.getEventSystem().addListener(EventSystem.EventType.LOGIN_SUCCESS, successListener);
+ clientCore.getEventSystem().addListener(EventSystem.EventType.LOGIN_FAILED, failureListener);
+
+ // Start login attempt
+ try {
+ loginState.setCredentials(username, password, otp);
+ loginState.attemptLogin();
+ } catch (Exception e) {
+ future.complete(new LoginResult(false, "Failed to start login: " + e.getMessage(), -1, null));
+ }
+
+ return future;
+ }
+
+ /**
+ * Set login callbacks for asynchronous login handling.
+ *
+ * @param onSuccess Called when login succeeds
+ * @param onFailure Called when login fails
+ * @param onProgress Called during login progress (optional)
+ */
+ public void setLoginCallbacks(Consumer onSuccess,
+ Consumer onFailure,
+ Consumer onProgress) {
+ this.loginSuccessCallback = onSuccess;
+ this.loginFailureCallback = onFailure;
+ this.loginProgressCallback = onProgress;
+ }
+
+ /**
+ * Enable or disable automatic reconnection.
+ *
+ * @param enabled Whether to enable auto-reconnection
+ * @param delaySeconds Delay between reconnection attempts
+ * @param maxAttempts Maximum number of reconnection attempts
+ */
+ public void setAutoReconnect(boolean enabled, int delaySeconds, int maxAttempts) {
+ this.autoReconnectEnabled = enabled;
+ this.autoReconnectDelaySeconds = delaySeconds;
+ this.maxReconnectAttempts = maxAttempts;
+
+ if (enabled) {
+ logger.info("Auto-reconnection enabled: delay={}s, maxAttempts={}", delaySeconds, maxAttempts);
+ } else {
+ logger.info("Auto-reconnection disabled");
+ }
+ }
+
+ /**
+ * Logout from the game.
+ */
+ public void logout() {
+ if (isLoggedIn()) {
+ clientCore.getLoginState().logout();
+ logger.info("Logout initiated");
+ }
+ }
+
+ /**
+ * Check if currently logged in.
+ */
+ public boolean isLoggedIn() {
+ return clientCore.getLoginState().getState() == LoginState.State.LOGGED_IN;
+ }
+
+ /**
+ * Check if login is in progress.
+ */
+ public boolean isLoginInProgress() {
+ return clientCore.getLoginState().getState() == LoginState.State.LOGGING_IN;
+ }
+
+ /**
+ * Get current login state.
+ */
+ public LoginState.State getLoginState() {
+ return clientCore.getLoginState().getState();
+ }
+
+ // ========== CONVENIENCE METHODS ==========
+
+ /**
+ * Get current player position (convenience method).
+ */
+ public Position getPlayerPosition() {
+ return playerAPI.getPosition();
+ }
+
+ /**
+ * Get current player health (convenience method).
+ */
+ public int getPlayerHealth() {
+ return playerAPI.getHitpoints();
+ }
+
+ /**
+ * Get current player energy (convenience method).
+ */
+ public int getPlayerEnergy() {
+ return playerAPI.getRunEnergy();
+ }
+
+ /**
+ * Check if player is moving (convenience method).
+ */
+ public boolean isPlayerMoving() {
+ return playerAPI.isMoving();
+ }
+
+ /**
+ * Get inventory item count (convenience method).
+ */
+ public int getInventoryItemCount(int itemId) {
+ return inventoryAPI.getItemCount(itemId);
+ }
+
+ /**
+ * Check if inventory contains item (convenience method).
+ */
+ public boolean hasInventoryItem(int itemId) {
+ return inventoryAPI.hasItem(itemId);
+ }
+
+ // ========== EVENT MONITORING ==========
+
+ /**
+ * Add a listener for specific event types.
+ */
+ public void addEventListener(EventSystem.EventType eventType, Consumer listener) {
+ clientCore.getEventSystem().addListener(eventType, listener);
+ }
+
+ /**
+ * Remove an event listener.
+ */
+ public void removeEventListener(EventSystem.EventType eventType, Consumer listener) {
+ clientCore.getEventSystem().removeListener(eventType, listener);
+ }
+
+ // ========== HELPER CLASSES ==========
+
+ /**
+ * Result of a login attempt.
+ */
+ public static class LoginResult {
+ private final boolean success;
+ private final String message;
+ private final int sessionId;
+ private final String sessionToken;
+
+ public LoginResult(boolean success, String message, int sessionId, String sessionToken) {
+ this.success = success;
+ this.message = message;
+ this.sessionId = sessionId;
+ this.sessionToken = sessionToken;
+ }
+
+ public boolean isSuccess() { return success; }
+ public String getMessage() { return message; }
+ public int getSessionId() { return sessionId; }
+ public String getSessionToken() { return sessionToken; }
+
+ @Override
+ public String toString() {
+ return "LoginResult{success=" + success + ", message='" + message + "', sessionId=" + sessionId + "}";
+ }
+ }
+
+ /**
+ * Login error information.
+ */
+ public static class LoginError {
+ private final int errorCode;
+ private final String errorMessage;
+
+ public LoginError(int errorCode, String errorMessage) {
+ this.errorCode = errorCode;
+ this.errorMessage = errorMessage;
+ }
+
+ public int getErrorCode() { return errorCode; }
+ public String getErrorMessage() { return errorMessage; }
+
+ @Override
+ public String toString() {
+ return "LoginError{code=" + errorCode + ", message='" + errorMessage + "'}";
+ }
+ }
+
+ // ========== PRIVATE METHODS ==========
+
+ private void setupEventListeners() {
+ // Listen for login events to trigger callbacks
+ clientCore.getEventSystem().addListener(EventSystem.EventType.LOGIN_SUCCESS, event -> {
+ if (loginSuccessCallback != null && event instanceof LoginState.LoginEvent) {
+ LoginState.LoginEvent loginEvent = (LoginState.LoginEvent) event;
+ LoginResult result = new LoginResult(true, "Login successful",
+ loginEvent.getSessionId(), loginEvent.getSessionToken());
+ loginSuccessCallback.accept(result);
+ }
+ currentReconnectAttempts = 0; // Reset reconnect counter on successful login
+ });
+
+ clientCore.getEventSystem().addListener(EventSystem.EventType.LOGIN_FAILED, event -> {
+ if (loginFailureCallback != null && event instanceof LoginState.LoginEvent) {
+ LoginState.LoginEvent loginEvent = (LoginState.LoginEvent) event;
+ LoginError error = new LoginError(loginEvent.getErrorCode(), loginEvent.getErrorMessage());
+ loginFailureCallback.accept(error);
+ }
+ });
+
+ clientCore.getEventSystem().addListener(EventSystem.EventType.LOGIN_PROGRESS, event -> {
+ if (loginProgressCallback != null && event instanceof LoginState.LoginEvent) {
+ LoginState.LoginEvent loginEvent = (LoginState.LoginEvent) event;
+ loginProgressCallback.accept(loginEvent.getProgressMessage());
+ }
+ });
+
+ // Listen for disconnection events for auto-reconnection
+ clientCore.getEventSystem().addListener(EventSystem.EventType.DISCONNECTED, event -> {
+ if (autoReconnectEnabled && currentReconnectAttempts < maxReconnectAttempts) {
+ handleAutoReconnect();
+ }
+ });
+ }
+
+ private void handleAutoReconnect() {
+ currentReconnectAttempts++;
+ logger.info("Auto-reconnection attempt {} of {}", currentReconnectAttempts, maxReconnectAttempts);
+
+ // Delay reconnection attempt
+ new Thread(() -> {
+ try {
+ Thread.sleep(autoReconnectDelaySeconds * 1000);
+
+ LoginState loginState = clientCore.getLoginState();
+ String lastUsername = loginState.getUsername();
+ String lastPassword = loginState.getPassword();
+ String lastOtp = loginState.getOtp();
+
+ if (!lastUsername.isEmpty() && !lastPassword.isEmpty()) {
+ logger.info("Attempting auto-reconnection for user: {}", lastUsername);
+ loginAsync(lastUsername, lastPassword, lastOtp);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }).start();
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/ApiDataClasses.java b/modernized-client/src/main/java/com/openosrs/client/api/ApiDataClasses.java
deleted file mode 100644
index 11e4b00..0000000
--- a/modernized-client/src/main/java/com/openosrs/client/api/ApiDataClasses.java
+++ /dev/null
@@ -1,463 +0,0 @@
-package com.openosrs.client.api;
-
-/**
- * Data classes and interfaces for the Agent API.
- */
-
-/**
- * Represents a position in the game world.
- */
-public class Position {
- private final int x;
- private final int y;
- private final int plane;
-
- public Position(int x, int y, int plane) {
- this.x = x;
- this.y = y;
- this.plane = plane;
- }
-
- public int getX() { return x; }
- public int getY() { return y; }
- public int getPlane() { return plane; }
-
- public double distanceTo(Position other) {
- if (other.plane != this.plane) {
- return Double.MAX_VALUE; // Different planes
- }
- int dx = other.x - this.x;
- int dy = other.y - this.y;
- return Math.sqrt(dx * dx + dy * dy);
- }
-
- public Position offset(int dx, int dy) {
- return new Position(x + dx, y + dy, plane);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) return true;
- if (!(obj instanceof Position)) return false;
- Position other = (Position) obj;
- return x == other.x && y == other.y && plane == other.plane;
- }
-
- @Override
- public int hashCode() {
- return 31 * (31 * x + y) + plane;
- }
-
- @Override
- public String toString() {
- return String.format("Position(%d, %d, %d)", x, y, plane);
- }
-}
-
-/**
- * Represents an NPC in the game world.
- */
-public class NPC {
- private final int index;
- private final int id;
- private final String name;
- private final Position position;
- private final int hitpoints;
- private final int maxHitpoints;
- private final int combatLevel;
- private final int animationId;
- private final boolean inCombat;
- private final String overheadText;
-
- public NPC(int index, int id, String name, Position position,
- int hitpoints, int maxHitpoints, int combatLevel,
- int animationId, boolean inCombat, String overheadText) {
- this.index = index;
- this.id = id;
- this.name = name;
- this.position = position;
- this.hitpoints = hitpoints;
- this.maxHitpoints = maxHitpoints;
- this.combatLevel = combatLevel;
- this.animationId = animationId;
- this.inCombat = inCombat;
- this.overheadText = overheadText;
- }
-
- public int getIndex() { return index; }
- public int getId() { return id; }
- public String getName() { return name; }
- public Position getPosition() { return position; }
- public int getHitpoints() { return hitpoints; }
- public int getMaxHitpoints() { return maxHitpoints; }
- public int getCombatLevel() { return combatLevel; }
- public int getAnimationId() { return animationId; }
- public boolean isInCombat() { return inCombat; }
- public String getOverheadText() { return overheadText; }
-
- public double distanceToPlayer(Position playerPos) {
- return position.distanceTo(playerPos);
- }
-
- public boolean isInteractable() {
- // NPCs with -1 ID are typically not interactable
- return id != -1;
- }
-
- @Override
- public String toString() {
- return String.format("NPC[%d] %s (%d) at %s", index, name, id, position);
- }
-}
-
-/**
- * Represents a game object (trees, rocks, doors, etc.).
- */
-public class GameObject {
- private final int id;
- private final String name;
- private final Position position;
- private final int type;
- private final int orientation;
- private final String[] actions;
-
- public GameObject(int id, String name, Position position, int type,
- int orientation, String[] actions) {
- this.id = id;
- this.name = name;
- this.position = position;
- this.type = type;
- this.orientation = orientation;
- this.actions = actions != null ? actions : new String[0];
- }
-
- public int getId() { return id; }
- public String getName() { return name; }
- public Position getPosition() { return position; }
- public int getType() { return type; }
- public int getOrientation() { return orientation; }
- public String[] getActions() { return actions.clone(); }
-
- public boolean hasAction(String action) {
- for (String a : actions) {
- if (a != null && a.equalsIgnoreCase(action)) {
- return true;
- }
- }
- return false;
- }
-
- public double distanceToPlayer(Position playerPos) {
- return position.distanceTo(playerPos);
- }
-
- @Override
- public String toString() {
- return String.format("GameObject[%d] %s at %s", id, name, position);
- }
-}
-
-/**
- * Represents an item on the ground.
- */
-public class GroundItem {
- private final int itemId;
- private final String name;
- private final int quantity;
- private final Position position;
- private final long spawnTime;
- private final boolean tradeable;
-
- public GroundItem(int itemId, String name, int quantity, Position position,
- long spawnTime, boolean tradeable) {
- this.itemId = itemId;
- this.name = name;
- this.quantity = quantity;
- this.position = position;
- this.spawnTime = spawnTime;
- this.tradeable = tradeable;
- }
-
- public int getItemId() { return itemId; }
- public String getName() { return name; }
- public int getQuantity() { return quantity; }
- public Position getPosition() { return position; }
- public long getSpawnTime() { return spawnTime; }
- public boolean isTradeable() { return tradeable; }
-
- public long getAge() {
- return System.currentTimeMillis() - spawnTime;
- }
-
- public double distanceToPlayer(Position playerPos) {
- return position.distanceTo(playerPos);
- }
-
- @Override
- public String toString() {
- return String.format("GroundItem[%d] %s x%d at %s", itemId, name, quantity, position);
- }
-}
-
-/**
- * Represents another player in the game.
- */
-public class OtherPlayer {
- private final int index;
- private final String username;
- private final Position position;
- private final int combatLevel;
- private final int animationId;
- private final String overheadText;
- private final boolean inCombat;
-
- public OtherPlayer(int index, String username, Position position,
- int combatLevel, int animationId, String overheadText, boolean inCombat) {
- this.index = index;
- this.username = username;
- this.position = position;
- this.combatLevel = combatLevel;
- this.animationId = animationId;
- this.overheadText = overheadText;
- this.inCombat = inCombat;
- }
-
- public int getIndex() { return index; }
- public String getUsername() { return username; }
- public Position getPosition() { return position; }
- public int getCombatLevel() { return combatLevel; }
- public int getAnimationId() { return animationId; }
- public String getOverheadText() { return overheadText; }
- public boolean isInCombat() { return inCombat; }
-
- public double distanceToPlayer(Position playerPos) {
- return position.distanceTo(playerPos);
- }
-
- @Override
- public String toString() {
- return String.format("Player[%d] %s (cb:%d) at %s", index, username, combatLevel, position);
- }
-}
-
-/**
- * Represents an item in inventory or equipment.
- */
-public class Item {
- private final int itemId;
- private final String name;
- private final int quantity;
- private final boolean stackable;
- private final boolean tradeable;
- private final String[] actions;
-
- public static final Item EMPTY = new Item(-1, "Empty", 0, false, false, new String[0]);
-
- public Item(int itemId, String name, int quantity, boolean stackable,
- boolean tradeable, String[] actions) {
- this.itemId = itemId;
- this.name = name;
- this.quantity = quantity;
- this.stackable = stackable;
- this.tradeable = tradeable;
- this.actions = actions != null ? actions : new String[0];
- }
-
- public int getItemId() { return itemId; }
- public String getName() { return name; }
- public int getQuantity() { return quantity; }
- public boolean isStackable() { return stackable; }
- public boolean isTradeable() { return tradeable; }
- public String[] getActions() { return actions.clone(); }
-
- public boolean isEmpty() {
- return itemId == -1 || quantity == 0;
- }
-
- public boolean hasAction(String action) {
- for (String a : actions) {
- if (a != null && a.equalsIgnoreCase(action)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public String toString() {
- return isEmpty() ? "Empty" : String.format("Item[%d] %s x%d", itemId, name, quantity);
- }
-}
-
-/**
- * Represents a path for navigation.
- */
-public class Path {
- private final Position[] steps;
- private final int length;
-
- public Path(Position[] steps) {
- this.steps = steps != null ? steps : new Position[0];
- this.length = this.steps.length;
- }
-
- public Position[] getSteps() { return steps.clone(); }
- public int getLength() { return length; }
- public boolean isEmpty() { return length == 0; }
-
- public Position getStep(int index) {
- if (index >= 0 && index < length) {
- return steps[index];
- }
- return null;
- }
-
- public Position getFirstStep() {
- return length > 0 ? steps[0] : null;
- }
-
- public Position getLastStep() {
- return length > 0 ? steps[length - 1] : null;
- }
-
- @Override
- public String toString() {
- return String.format("Path[%d steps]", length);
- }
-}
-
-/**
- * Event data classes for API events.
- */
-
-public class HealthInfo {
- private final int current;
- private final int max;
- private final double percentage;
-
- public HealthInfo(int current, int max) {
- this.current = current;
- this.max = max;
- this.percentage = max > 0 ? (double) current / max : 0.0;
- }
-
- public int getCurrent() { return current; }
- public int getMax() { return max; }
- public double getPercentage() { return percentage; }
-
- public boolean isLow() { return percentage < 0.3; }
- public boolean isCritical() { return percentage < 0.15; }
-
- @Override
- public String toString() {
- return String.format("Health: %d/%d (%.1f%%)", current, max, percentage * 100);
- }
-}
-
-public class InventoryChange {
- private final int slot;
- private final Item oldItem;
- private final Item newItem;
-
- public InventoryChange(int slot, Item oldItem, Item newItem) {
- this.slot = slot;
- this.oldItem = oldItem;
- this.newItem = newItem;
- }
-
- public int getSlot() { return slot; }
- public Item getOldItem() { return oldItem; }
- public Item getNewItem() { return newItem; }
-
- public boolean isItemAdded() {
- return oldItem.isEmpty() && !newItem.isEmpty();
- }
-
- public boolean isItemRemoved() {
- return !oldItem.isEmpty() && newItem.isEmpty();
- }
-
- public boolean isQuantityChanged() {
- return oldItem.getItemId() == newItem.getItemId() &&
- oldItem.getQuantity() != newItem.getQuantity();
- }
-
- @Override
- public String toString() {
- return String.format("InventoryChange[%d]: %s -> %s", slot, oldItem, newItem);
- }
-}
-
-public class ExperienceGain {
- private final AgentAPI.Skill skill;
- private final int oldExperience;
- private final int newExperience;
- private final int gain;
-
- public ExperienceGain(AgentAPI.Skill skill, int oldExperience, int newExperience) {
- this.skill = skill;
- this.oldExperience = oldExperience;
- this.newExperience = newExperience;
- this.gain = newExperience - oldExperience;
- }
-
- public AgentAPI.Skill getSkill() { return skill; }
- public int getOldExperience() { return oldExperience; }
- public int getNewExperience() { return newExperience; }
- public int getGain() { return gain; }
-
- @Override
- public String toString() {
- return String.format("ExperienceGain: %s +%d xp (total: %d)",
- skill.name(), gain, newExperience);
- }
-}
-
-public class CombatStateChange {
- private final boolean inCombat;
- private final NPC target;
-
- public CombatStateChange(boolean inCombat, NPC target) {
- this.inCombat = inCombat;
- this.target = target;
- }
-
- public boolean isInCombat() { return inCombat; }
- public NPC getTarget() { return target; }
-
- @Override
- public String toString() {
- return String.format("CombatStateChange: %s%s",
- inCombat ? "Entered combat" : "Exited combat",
- target != null ? " with " + target.getName() : "");
- }
-}
-
-public class ChatMessage {
- private final String username;
- private final String message;
- private final int type;
- private final long timestamp;
-
- public ChatMessage(String username, String message, int type) {
- this.username = username;
- this.message = message;
- this.type = type;
- this.timestamp = System.currentTimeMillis();
- }
-
- public String getUsername() { return username; }
- public String getMessage() { return message; }
- public int getType() { return type; }
- public long getTimestamp() { return timestamp; }
-
- public boolean isPublicChat() { return type == 0; }
- public boolean isPrivateMessage() { return type == 3; }
- public boolean isFriendChat() { return type == 9; }
- public boolean isClanChat() { return type == 11; }
-
- @Override
- public String toString() {
- return String.format("ChatMessage[%s]: %s", username, message);
- }
-}
\ No newline at end of file
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/ApiModules.java b/modernized-client/src/main/java/com/openosrs/client/api/ApiModules.java
deleted file mode 100644
index 2cc6cc1..0000000
--- a/modernized-client/src/main/java/com/openosrs/client/api/ApiModules.java
+++ /dev/null
@@ -1,499 +0,0 @@
-package com.openosrs.client.api;
-
-import com.openosrs.client.core.ClientCore;
-import com.openosrs.client.core.GameNPC;
-import com.openosrs.client.core.EventSystem;
-import com.openosrs.client.engine.GameEngine;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-
-/**
- * API module implementations.
- */
-
-/**
- * PlayerAPI - Handles player-specific queries.
- */
-class PlayerAPI {
- private final ClientCore clientCore;
-
- public PlayerAPI(ClientCore clientCore) {
- this.clientCore = clientCore;
- }
-
- public Position getPosition() {
- var playerState = clientCore.getPlayerState();
- return new Position(playerState.getWorldX(), playerState.getWorldY(), playerState.getPlane());
- }
-
- public int getHitpoints() {
- return clientCore.getPlayerState().getHitpoints();
- }
-
- public int getMaxHitpoints() {
- return clientCore.getPlayerState().getMaxHitpoints();
- }
-
- public int getPrayer() {
- return clientCore.getPlayerState().getPrayer();
- }
-
- public int getMaxPrayer() {
- return clientCore.getPlayerState().getMaxPrayer();
- }
-
- public int getRunEnergy() {
- return clientCore.getPlayerState().getRunEnergy();
- }
-
- public int getCombatLevel() {
- return clientCore.getPlayerState().getCombatLevel();
- }
-
- public int getSkillLevel(AgentAPI.Skill skill) {
- return clientCore.getPlayerState().getSkillLevel(skill.getId());
- }
-
- public int getBoostedSkillLevel(AgentAPI.Skill skill) {
- return clientCore.getPlayerState().getBoostedSkillLevel(skill.getId());
- }
-
- public int getSkillExperience(AgentAPI.Skill skill) {
- return clientCore.getPlayerState().getSkillExperience(skill.getId());
- }
-
- public boolean isInCombat() {
- return clientCore.getPlayerState().isInCombat();
- }
-
- public int getCurrentAnimation() {
- return clientCore.getPlayerState().getAnimationId();
- }
-}
-
-/**
- * WorldAPI - Handles world object queries.
- */
-class WorldAPI {
- private final ClientCore clientCore;
-
- public WorldAPI(ClientCore clientCore) {
- this.clientCore = clientCore;
- }
-
- public List getNPCs() {
- return clientCore.getWorldState().getAllNPCs().stream()
- .map(this::convertNPC)
- .collect(Collectors.toList());
- }
-
- public List getNPCsById(int npcId) {
- return clientCore.getWorldState().getNPCsById(npcId).stream()
- .map(this::convertNPC)
- .collect(Collectors.toList());
- }
-
- public NPC getClosestNPC() {
- Position playerPos = new PlayerAPI(clientCore).getPosition();
- return getNPCs().stream()
- .min((n1, n2) -> Double.compare(
- n1.distanceToPlayer(playerPos),
- n2.distanceToPlayer(playerPos)))
- .orElse(null);
- }
-
- public NPC getClosestNPC(int npcId) {
- Position playerPos = new PlayerAPI(clientCore).getPosition();
- return getNPCsById(npcId).stream()
- .min((n1, n2) -> Double.compare(
- n1.distanceToPlayer(playerPos),
- n2.distanceToPlayer(playerPos)))
- .orElse(null);
- }
-
- public List getNPCsInRadius(Position center, int radius) {
- return getNPCs().stream()
- .filter(npc -> npc.getPosition().distanceTo(center) <= radius)
- .collect(Collectors.toList());
- }
-
- public List getGameObjects() {
- return clientCore.getWorldState().getAllObjects().stream()
- .map(this::convertGameObject)
- .collect(Collectors.toList());
- }
-
- public List getGameObjectsById(int objectId) {
- return clientCore.getWorldState().getObjectsById(objectId).stream()
- .map(this::convertGameObject)
- .collect(Collectors.toList());
- }
-
- public GameObject getClosestGameObject(int objectId) {
- Position playerPos = new PlayerAPI(clientCore).getPosition();
- return getGameObjectsById(objectId).stream()
- .min((o1, o2) -> Double.compare(
- o1.distanceToPlayer(playerPos),
- o2.distanceToPlayer(playerPos)))
- .orElse(null);
- }
-
- public List getGroundItems() {
- return clientCore.getWorldState().getAllGroundItems().stream()
- .map(this::convertGroundItem)
- .collect(Collectors.toList());
- }
-
- public List getGroundItemsById(int itemId) {
- return getGroundItems().stream()
- .filter(item -> item.getItemId() == itemId)
- .collect(Collectors.toList());
- }
-
- public GroundItem getClosestGroundItem(int itemId) {
- Position playerPos = new PlayerAPI(clientCore).getPosition();
- return getGroundItemsById(itemId).stream()
- .min((i1, i2) -> Double.compare(
- i1.distanceToPlayer(playerPos),
- i2.distanceToPlayer(playerPos)))
- .orElse(null);
- }
-
- public List getOtherPlayers() {
- return clientCore.getWorldState().getAllOtherPlayers().stream()
- .map(this::convertOtherPlayer)
- .collect(Collectors.toList());
- }
-
- private NPC convertNPC(GameNPC gameNPC) {
- Position pos = new Position(gameNPC.getX(), gameNPC.getY(), gameNPC.getPlane());
- return new NPC(
- gameNPC.getIndex(),
- gameNPC.getId(),
- gameNPC.getName(),
- pos,
- gameNPC.getHitpoints(),
- gameNPC.getMaxHitpoints(),
- gameNPC.getCombatLevel(),
- gameNPC.getAnimationId(),
- gameNPC.getInteracting() != -1,
- gameNPC.getOverheadText()
- );
- }
-
- private GameObject convertGameObject(com.openosrs.client.core.GameObject coreObject) {
- Position pos = new Position(coreObject.getX(), coreObject.getY(), coreObject.getPlane());
- return new GameObject(
- coreObject.getId(),
- "Object " + coreObject.getId(), // Name would come from definitions
- pos,
- coreObject.getType(),
- coreObject.getOrientation(),
- new String[]{"Examine"} // Actions would come from definitions
- );
- }
-
- private GroundItem convertGroundItem(com.openosrs.client.core.GroundItem coreItem) {
- Position pos = new Position(coreItem.getX(), coreItem.getY(), coreItem.getPlane());
- return new GroundItem(
- coreItem.getItemId(),
- "Item " + coreItem.getItemId(), // Name would come from definitions
- coreItem.getQuantity(),
- pos,
- coreItem.getSpawnTime(),
- true // Tradeable would come from definitions
- );
- }
-
- private OtherPlayer convertOtherPlayer(com.openosrs.client.core.OtherPlayer corePlayer) {
- Position pos = new Position(corePlayer.getX(), corePlayer.getY(), corePlayer.getPlane());
- return new OtherPlayer(
- corePlayer.getIndex(),
- corePlayer.getUsername(),
- pos,
- corePlayer.getCombatLevel(),
- corePlayer.getAnimationId(),
- corePlayer.getOverheadText(),
- false // Combat state would be determined from game state
- );
- }
-}
-
-/**
- * InventoryAPI - Handles inventory and equipment queries.
- */
-class InventoryAPI {
- private final ClientCore clientCore;
-
- public InventoryAPI(ClientCore clientCore) {
- this.clientCore = clientCore;
- }
-
- public Item[] getInventory() {
- var inventory = clientCore.getInventoryState().getInventory();
- Item[] items = new Item[inventory.length];
-
- for (int i = 0; i < inventory.length; i++) {
- items[i] = convertItemStack(inventory[i]);
- }
-
- return items;
- }
-
- public Item getInventorySlot(int slot) {
- var itemStack = clientCore.getInventoryState().getInventoryItem(slot);
- return convertItemStack(itemStack);
- }
-
- public boolean hasItem(int itemId) {
- return clientCore.getInventoryState().hasItem(itemId);
- }
-
- public int getItemCount(int itemId) {
- return clientCore.getInventoryState().getItemQuantity(itemId);
- }
-
- public int findItemSlot(int itemId) {
- return clientCore.getInventoryState().findItemSlot(itemId);
- }
-
- public boolean isInventoryFull() {
- return clientCore.getInventoryState().isInventoryFull();
- }
-
- public int getEmptySlots() {
- return clientCore.getInventoryState().getEmptySlots();
- }
-
- public Item[] getEquipment() {
- var equipment = clientCore.getInventoryState().getEquipment();
- Item[] items = new Item[equipment.length];
-
- for (int i = 0; i < equipment.length; i++) {
- items[i] = convertItemStack(equipment[i]);
- }
-
- return items;
- }
-
- public Item getEquipmentSlot(AgentAPI.EquipmentSlot slot) {
- var itemStack = clientCore.getInventoryState().getEquipmentItem(slot.getId());
- return convertItemStack(itemStack);
- }
-
- private Item convertItemStack(com.openosrs.client.core.InventoryState.ItemStack itemStack) {
- if (itemStack.isEmpty()) {
- return Item.EMPTY;
- }
-
- return new Item(
- itemStack.getItemId(),
- "Item " + itemStack.getItemId(), // Name would come from definitions
- itemStack.getQuantity(),
- false, // Stackable would come from definitions
- true, // Tradeable would come from definitions
- new String[]{"Use", "Drop"} // Actions would come from definitions
- );
- }
-}
-
-/**
- * InteractionAPI - Handles player actions and interactions.
- */
-class InteractionAPI {
- private static final Logger logger = LoggerFactory.getLogger(InteractionAPI.class);
-
- private final ClientCore clientCore;
- private final GameEngine gameEngine;
-
- public InteractionAPI(ClientCore clientCore, GameEngine gameEngine) {
- this.clientCore = clientCore;
- this.gameEngine = gameEngine;
- }
-
- public CompletableFuture walkTo(Position position) {
- logger.debug("Walking to {}", position);
-
- return CompletableFuture.supplyAsync(() -> {
- try {
- // Send movement packet
- gameEngine.getNetworkEngine().sendMovement(
- position.getX(),
- position.getY(),
- false // walking, not running
- );
-
- // Wait for movement to complete
- Position startPos = new PlayerAPI(clientCore).getPosition();
- long startTime = System.currentTimeMillis();
-
- while (System.currentTimeMillis() - startTime < 10000) { // 10 second timeout
- Position currentPos = new PlayerAPI(clientCore).getPosition();
- if (currentPos.distanceTo(position) < 2.0) {
- return true;
- }
- Thread.sleep(100);
- }
-
- return false; // Timeout
-
- } catch (Exception e) {
- logger.error("Error walking to position", e);
- return false;
- }
- });
- }
-
- public CompletableFuture runTo(Position position) {
- logger.debug("Running to {}", position);
-
- return CompletableFuture.supplyAsync(() -> {
- try {
- // Send movement packet with running flag
- gameEngine.getNetworkEngine().sendMovement(
- position.getX(),
- position.getY(),
- true // running
- );
-
- // Wait for movement to complete (similar to walkTo but faster)
- Position startPos = new PlayerAPI(clientCore).getPosition();
- long startTime = System.currentTimeMillis();
-
- while (System.currentTimeMillis() - startTime < 8000) { // 8 second timeout
- Position currentPos = new PlayerAPI(clientCore).getPosition();
- if (currentPos.distanceTo(position) < 2.0) {
- return true;
- }
- Thread.sleep(100);
- }
-
- return false; // Timeout
-
- } catch (Exception e) {
- logger.error("Error running to position", e);
- return false;
- }
- });
- }
-
- public CompletableFuture interactWithNPC(NPC npc, String action) {
- logger.debug("Interacting with NPC {} with action '{}'", npc.getName(), action);
-
- return CompletableFuture.supplyAsync(() -> {
- try {
- // Send NPC interaction packet
- gameEngine.getNetworkEngine().sendNPCInteraction(npc.getIndex(), 1);
-
- // Wait for interaction to process
- Thread.sleep(600); // Standard interaction delay
- return true;
-
- } catch (Exception e) {
- logger.error("Error interacting with NPC", e);
- return false;
- }
- });
- }
-
- public CompletableFuture interactWithObject(GameObject object, String action) {
- logger.debug("Interacting with object {} with action '{}'", object.getName(), action);
-
- return CompletableFuture.supplyAsync(() -> {
- try {
- // Send object interaction packet
- Position pos = object.getPosition();
- gameEngine.getNetworkEngine().sendObjectInteraction(
- object.getId(), pos.getX(), pos.getY(), 1);
-
- // Wait for interaction to process
- Thread.sleep(600); // Standard interaction delay
- return true;
-
- } catch (Exception e) {
- logger.error("Error interacting with object", e);
- return false;
- }
- });
- }
-
- public CompletableFuture pickupItem(GroundItem item) {
- logger.debug("Picking up item {}", item.getName());
-
- return CompletableFuture.supplyAsync(() -> {
- try {
- // Move to item first if not adjacent
- Position playerPos = new PlayerAPI(clientCore).getPosition();
- if (item.distanceToPlayer(playerPos) > 1.0) {
- walkTo(item.getPosition()).get();
- }
-
- // Send pickup packet (would be implemented in network engine)
- Thread.sleep(600);
- return true;
-
- } catch (Exception e) {
- logger.error("Error picking up item", e);
- return false;
- }
- });
- }
-
- public CompletableFuture useItem(int slot) {
- logger.debug("Using item in slot {}", slot);
-
- return CompletableFuture.supplyAsync(() -> {
- try {
- // Send use item packet
- Thread.sleep(600);
- return true;
-
- } catch (Exception e) {
- logger.error("Error using item", e);
- return false;
- }
- });
- }
-
- public CompletableFuture useItemOnItem(int sourceSlot, int targetSlot) {
- logger.debug("Using item {} on item {}", sourceSlot, targetSlot);
-
- return CompletableFuture.supplyAsync(() -> {
- try {
- // Send use item on item packet
- Thread.sleep(600);
- return true;
-
- } catch (Exception e) {
- logger.error("Error using item on item", e);
- return false;
- }
- });
- }
-
- public CompletableFuture dropItem(int slot) {
- logger.debug("Dropping item in slot {}", slot);
-
- return CompletableFuture.supplyAsync(() -> {
- try {
- // Send drop item packet
- Thread.sleep(600);
- return true;
-
- } catch (Exception e) {
- logger.error("Error dropping item", e);
- return false;
- }
- });
- }
-
- public void sendChatMessage(String message) {
- logger.debug("Sending chat message: {}", message);
- gameEngine.getNetworkEngine().sendChatMessage(message);
- }
-}
\ No newline at end of file
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/CombatAPI.java b/modernized-client/src/main/java/com/openosrs/client/api/CombatAPI.java
index 3007a29..640fe37 100644
--- a/modernized-client/src/main/java/com/openosrs/client/api/CombatAPI.java
+++ b/modernized-client/src/main/java/com/openosrs/client/api/CombatAPI.java
@@ -1,7 +1,11 @@
package com.openosrs.client.api;
import com.openosrs.client.core.ClientCore;
-import com.openosrs.client.engine.GameEngine;
+import com.openosrs.client.core.bridge.BridgeAdapter;
+import com.openosrs.client.api.types.Item;
+import com.openosrs.client.api.types.NPC;
+import com.openosrs.client.api.types.Position;
+import com.openosrs.client.api.types.Skill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -10,7 +14,7 @@ import java.util.concurrent.CompletableFuture;
/**
* CombatAPI - Handles combat-related actions and state.
- *
+ *
* This API module provides methods for:
* - Combat actions (attacking, eating, drinking potions)
* - Combat state monitoring
@@ -19,7 +23,347 @@ import java.util.concurrent.CompletableFuture;
*/
public class CombatAPI {
private static final Logger logger = LoggerFactory.getLogger(CombatAPI.class);
-
+
private final ClientCore clientCore;
- private final GameEngine gameEngine;
- \n // Common food item IDs (for auto-eating)\n private static final int[] FOOD_IDS = {\n 385, // Shark\n 379, // Lobster\n 373, // Swordfish\n 365, // Bass\n 361, // Tuna\n 329, // Salmon\n 315, // Shrimps\n 7946, // Monkfish\n 3144, // Karambwan\n 6297, // Anglerfish\n 385, // Shark\n 12625, // Saradomin brew\n };\n \n // Common potion IDs\n private static final int[] COMBAT_POTIONS = {\n 2428, // Attack potion(4)\n 113, // Strength potion(4)\n 2440, // Super attack(4)\n 2442, // Super strength(4)\n 2436, // Super defence(4)\n 2444, // Ranging potion(4)\n 3024, // Super energy(4)\n 3016, // Energy potion(4)\n 2434, // Prayer potion(4)\n 12625, // Saradomin brew(4)\n 12631, // Super restore(4)\n };\n \n public CombatAPI(ClientCore clientCore, GameEngine gameEngine) {\n this.clientCore = clientCore;\n this.gameEngine = gameEngine;\n }\n \n /**\n * Attack an NPC.\n */\n public CompletableFuture attackNPC(NPC npc) {\n if (npc == null) {\n return CompletableFuture.completedFuture(false);\n }\n \n logger.debug(\"Attacking NPC: {} (ID: {})\", npc.getName(), npc.getId());\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n // Check if NPC is attackable\n if (!npc.isInteractable()) {\n logger.warn(\"NPC {} is not attackable\", npc.getName());\n return false;\n }\n \n Position playerPos = clientCore.getPlayerState().getPosition();\n if (playerPos == null) {\n logger.warn(\"Cannot attack - player position unknown\");\n return false;\n }\n \n double distance = npc.distanceToPlayer(playerPos);\n if (distance > 10) {\n logger.warn(\"NPC too far away for attack: {} tiles\", distance);\n return false;\n }\n \n // Check if already in combat\n if (clientCore.getPlayerState().isInCombat()) {\n logger.debug(\"Already in combat, switching targets\");\n }\n \n // TODO: Send attack packet to server\n // TODO: Update combat state\n \n // Simulate attack time\n Thread.sleep(600); // 1 game tick\n \n logger.debug(\"Attack initiated on NPC: {}\", npc.getName());\n return true;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"Attack interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error during attack\", e);\n return false;\n }\n });\n }\n \n /**\n * Eat food from inventory (automatically finds food).\n */\n public CompletableFuture eatFood() {\n logger.debug(\"Attempting to eat food\");\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n // Find food in inventory\n Item[] inventory = clientCore.getInventoryState().getInventory();\n \n for (int slot = 0; slot < inventory.length; slot++) {\n Item item = inventory[slot];\n if (item != null && !item.isEmpty() && isFood(item.getItemId())) {\n logger.debug(\"Eating food: {}\", item.getName());\n \n // TODO: Send eat packet to server\n // TODO: Update player health\n \n // Simulate eating time\n Thread.sleep(1800); // 3 game ticks\n \n logger.debug(\"Food consumed: {}\", item.getName());\n return true;\n }\n }\n \n logger.warn(\"No food found in inventory\");\n return false;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"Eating interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error eating food\", e);\n return false;\n }\n });\n }\n \n /**\n * Drink a specific potion.\n */\n public CompletableFuture drinkPotion(int itemId) {\n logger.debug(\"Attempting to drink potion: {}\", itemId);\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n // Find potion in inventory\n int slot = clientCore.getInventoryState().findItemSlot(itemId);\n if (slot == -1) {\n logger.warn(\"Potion {} not found in inventory\", itemId);\n return false;\n }\n \n Item potion = clientCore.getInventoryState().getInventorySlot(slot);\n logger.debug(\"Drinking potion: {}\", potion.getName());\n \n // TODO: Send drink packet to server\n // TODO: Update player stats based on potion effect\n \n // Simulate drinking time\n Thread.sleep(1800); // 3 game ticks\n \n logger.debug(\"Potion consumed: {}\", potion.getName());\n return true;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"Drinking interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error drinking potion\", e);\n return false;\n }\n });\n }\n \n /**\n * Get current combat target.\n */\n public NPC getCombatTarget() {\n // TODO: Track current combat target\n // For now, return null as we don't have combat state tracking yet\n return null;\n }\n \n /**\n * Check if the player should eat (low health).\n */\n public boolean shouldEat() {\n int currentHp = clientCore.getPlayerState().getSkillLevel(AgentAPI.Skill.HITPOINTS);\n int maxHp = clientCore.getPlayerState().getSkillRealLevel(AgentAPI.Skill.HITPOINTS);\n \n double healthPercentage = (double) currentHp / maxHp;\n return healthPercentage < 0.6; // Eat when below 60% health\n }\n \n /**\n * Check if the player should drink a prayer potion.\n */\n public boolean shouldDrinkPrayerPotion() {\n int currentPrayer = clientCore.getPlayerState().getSkillLevel(AgentAPI.Skill.PRAYER);\n int maxPrayer = clientCore.getPlayerState().getSkillRealLevel(AgentAPI.Skill.PRAYER);\n \n double prayerPercentage = (double) currentPrayer / maxPrayer;\n return prayerPercentage < 0.25; // Drink when below 25% prayer\n }\n \n /**\n * Automatically handle combat (eat food, drink potions if needed).\n */\n public CompletableFuture autoManageCombat() {\n return CompletableFuture.supplyAsync(() -> {\n try {\n boolean actionTaken = false;\n \n // Check if we need to eat\n if (shouldEat()) {\n logger.debug(\"Health low, attempting to eat\");\n if (eatFood().get()) {\n actionTaken = true;\n }\n }\n \n // Check if we need prayer\n if (shouldDrinkPrayerPotion()) {\n logger.debug(\"Prayer low, attempting to drink prayer potion\");\n // Try to drink prayer potion (4)\n if (drinkPotion(2434).get()) {\n actionTaken = true;\n }\n }\n \n return actionTaken;\n \n } catch (Exception e) {\n logger.error(\"Error during auto combat management\", e);\n return false;\n }\n });\n }\n \n /**\n * Check if an item ID is food.\n */\n private boolean isFood(int itemId) {\n return Arrays.stream(FOOD_IDS).anyMatch(foodId -> foodId == itemId);\n }\n \n /**\n * Check if an item ID is a combat potion.\n */\n private boolean isCombatPotion(int itemId) {\n return Arrays.stream(COMBAT_POTIONS).anyMatch(potionId -> potionId == itemId);\n }\n \n /**\n * Calculate the player's max hit (simplified).\n */\n public int calculateMaxHit() {\n // TODO: Implement proper max hit calculation\n // This would need to consider strength level, equipment, potions, etc.\n int strLevel = clientCore.getPlayerState().getSkillLevel(AgentAPI.Skill.STRENGTH);\n return Math.max(1, strLevel / 10); // Very simplified calculation\n }\n \n /**\n * Calculate combat effectiveness against an NPC.\n */\n public double calculateCombatEffectiveness(NPC npc) {\n if (npc == null) {\n return 0.0;\n }\n \n // TODO: Implement proper effectiveness calculation\n // This would consider combat levels, equipment, NPC defence, etc.\n int playerCombatLevel = clientCore.getPlayerState().getCombatLevel();\n int npcCombatLevel = npc.getCombatLevel();\n \n if (npcCombatLevel == 0) {\n return 1.0; // Non-combat NPC\n }\n \n return Math.min(1.0, (double) playerCombatLevel / npcCombatLevel);\n }\n \n /**\n * Check if it's safe to engage in combat.\n */\n public boolean isSafeToCombat() {\n // Check health\n if (shouldEat() && !hasFood()) {\n logger.warn(\"Not safe to combat - low health and no food\");\n return false;\n }\n \n // Check if already in dangerous combat\n int currentHp = clientCore.getPlayerState().getSkillLevel(AgentAPI.Skill.HITPOINTS);\n if (currentHp <= 20 && clientCore.getPlayerState().isInCombat()) {\n logger.warn(\"Not safe to combat - critical health in combat\");\n return false;\n }\n \n return true;\n }\n \n /**\n * Check if the player has food in inventory.\n */\n public boolean hasFood() {\n Item[] inventory = clientCore.getInventoryState().getInventory();\n \n for (Item item : inventory) {\n if (item != null && !item.isEmpty() && isFood(item.getItemId())) {\n return true;\n }\n }\n \n return false;\n }\n \n /**\n * Get the amount of food in inventory.\n */\n public int getFoodCount() {\n Item[] inventory = clientCore.getInventoryState().getInventory();\n int count = 0;\n \n for (Item item : inventory) {\n if (item != null && !item.isEmpty() && isFood(item.getItemId())) {\n count += item.getQuantity();\n }\n }\n \n return count;\n }\n}"
\ No newline at end of file
+ private final BridgeAdapter bridgeAdapter;
+
+ // Common food item IDs (for auto-eating)
+ private static final int[] FOOD_IDS = {
+ 385, // Shark
+ 379, // Lobster
+ 373, // Swordfish
+ 365, // Bass
+ 361, // Tuna
+ 329, // Salmon
+ 315, // Shrimps
+ 7946, // Monkfish
+ 3144, // Karambwan
+ 6297, // Anglerfish
+ 385, // Shark
+ 12625, // Saradomin brew
+ };
+
+ // Common potion IDs
+ private static final int[] COMBAT_POTIONS = {
+ 2428, // Attack potion(4)
+ 113, // Strength potion(4)
+ 2440, // Super attack(4)
+ 2442, // Super strength(4)
+ 2436, // Super defence(4)
+ 2444, // Ranging potion(4)
+ 3024, // Super energy(4)
+ 3016, // Energy potion(4)
+ 2434, // Prayer potion(4)
+ 12625, // Saradomin brew(4)
+ 12631, // Super restore(4)
+ };
+
+ public CombatAPI(ClientCore clientCore, BridgeAdapter bridgeAdapter) {
+ this.clientCore = clientCore;
+ this.bridgeAdapter = bridgeAdapter;
+ }
+
+ /**
+ * Attack an NPC.
+ */
+ public CompletableFuture attackNPC(NPC npc) {
+ if (npc == null) {
+ return CompletableFuture.completedFuture(false);
+ }
+
+ logger.debug("Attacking NPC: {} (ID: {})", npc.getName(), npc.getId());
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // Check if NPC is attackable
+ if (!npc.isInteractable()) {
+ logger.warn("NPC {} is not attackable", npc.getName());
+ return false;
+ }
+
+ Position playerPos = bridgeAdapter.getPlayerPosition();
+ if (playerPos == null) {
+ logger.warn("Cannot attack - player position unknown");
+ return false;
+ }
+
+ double distance = npc.getPosition().distanceTo(playerPos);
+ if (distance > 10) {
+ logger.warn("NPC too far away for attack: {} tiles", distance);
+ return false;
+ }
+
+ // Check if already in combat
+ if (clientCore.getPlayerState().isInCombat()) {
+ logger.debug("Already in combat, switching targets");
+ }
+
+ // TODO: Send attack packet to server
+ // TODO: Update combat state
+
+ // Simulate attack time
+ Thread.sleep(600); // 1 game tick
+
+ logger.debug("Attack initiated on NPC: {}", npc.getName());
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("Attack interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error during attack", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Eat food from inventory (automatically finds food).
+ */
+ public CompletableFuture eatFood() {
+ logger.debug("Attempting to eat food");
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // Find food in inventory
+ Item[] inventory = bridgeAdapter.getInventory();
+
+ for (int slot = 0; slot < inventory.length; slot++) {
+ Item item = inventory[slot];
+ if (item != null && !item.isEmpty() && isFood(item.getId())) {
+ logger.debug("Eating food: {}", item.getName());
+
+ // TODO: Send eat packet to server
+ // TODO: Update player health
+
+ // Simulate eating time
+ Thread.sleep(1800); // 3 game ticks
+
+ logger.debug("Food consumed: {}", item.getName());
+ return true;
+ }
+ }
+
+ logger.warn("No food found in inventory");
+ return false;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("Eating interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error eating food", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Drink a specific potion.
+ */
+ public CompletableFuture drinkPotion(int itemId) {
+ logger.debug("Attempting to drink potion: {}", itemId);
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // Find potion in inventory
+ Item[] inventory = bridgeAdapter.getInventory();
+ Item potion = null;
+ int foundSlot = -1;
+
+ for (int slot = 0; slot < inventory.length; slot++) {
+ Item item = inventory[slot];
+ if (item != null && !item.isEmpty() && item.getId() == itemId) {
+ potion = item;
+ foundSlot = slot;
+ break;
+ }
+ }
+
+ if (potion == null) {
+ logger.warn("Potion {} not found in inventory", itemId);
+ return false;
+ }
+ logger.debug("Drinking potion: {}", potion.getName());
+
+ // TODO: Send drink packet to server
+ // TODO: Update player stats based on potion effect
+
+ // Simulate drinking time
+ Thread.sleep(1800); // 3 game ticks
+
+ logger.debug("Potion consumed: {}", potion.getName());
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("Drinking interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error drinking potion", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Get current combat target.
+ */
+ public NPC getCombatTarget() {
+ // TODO: Track current combat target
+ // For now, return null as we don't have combat state tracking yet
+ return null;
+ }
+
+ /**
+ * Check if the player should eat (low health).
+ */
+ public boolean shouldEat() {
+ int currentHp = bridgeAdapter.getSkillLevel(Skill.HITPOINTS);
+ int maxHp = bridgeAdapter.getSkillRealLevel(Skill.HITPOINTS);
+
+ double healthPercentage = (double) currentHp / maxHp;
+ return healthPercentage < 0.6; // Eat when below 60% health
+ }
+
+ /**
+ * Check if the player should drink a prayer potion.
+ */
+ public boolean shouldDrinkPrayerPotion() {
+ int currentPrayer = bridgeAdapter.getSkillLevel(Skill.PRAYER);
+ int maxPrayer = bridgeAdapter.getSkillRealLevel(Skill.PRAYER);
+
+ double prayerPercentage = (double) currentPrayer / maxPrayer;
+ return prayerPercentage < 0.25; // Drink when below 25% prayer
+ }
+
+ /**
+ * Automatically handle combat (eat food, drink potions if needed).
+ */
+ public CompletableFuture autoManageCombat() {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ boolean actionTaken = false;
+
+ // Check if we need to eat
+ if (shouldEat()) {
+ logger.debug("Health low, attempting to eat");
+ if (eatFood().get()) {
+ actionTaken = true;
+ }
+ }
+
+ // Check if we need prayer
+ if (shouldDrinkPrayerPotion()) {
+ logger.debug("Prayer low, attempting to drink prayer potion");
+ // Try to drink prayer potion (4)
+ if (drinkPotion(2434).get()) {
+ actionTaken = true;
+ }
+ }
+
+ return actionTaken;
+
+ } catch (Exception e) {
+ logger.error("Error during auto combat management", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Check if an item ID is food.
+ */
+ private boolean isFood(int itemId) {
+ return Arrays.stream(FOOD_IDS).anyMatch(foodId -> foodId == itemId);
+ }
+
+ /**
+ * Check if an item ID is a combat potion.
+ */
+ private boolean isCombatPotion(int itemId) {
+ return Arrays.stream(COMBAT_POTIONS).anyMatch(potionId -> potionId == itemId);
+ }
+
+ /**
+ * Calculate the player's max hit (simplified).
+ */
+ public int calculateMaxHit() {
+ // TODO: Implement proper max hit calculation
+ // This would need to consider strength level, equipment, potions, etc.
+ int strLevel = bridgeAdapter.getSkillLevel(Skill.STRENGTH);
+ return Math.max(1, strLevel / 10); // Very simplified calculation
+ }
+
+ /**
+ * Calculate combat effectiveness against an NPC.
+ */
+ public double calculateCombatEffectiveness(NPC npc) {
+ if (npc == null) {
+ return 0.0;
+ }
+
+ // TODO: Implement proper effectiveness calculation
+ // This would consider combat levels, equipment, NPC defence, etc.
+ int playerCombatLevel = clientCore.getPlayerState().getCombatLevel();
+ int npcCombatLevel = npc.getCombatLevel();
+
+ if (npcCombatLevel == 0) {
+ return 1.0; // Non-combat NPC
+ }
+
+ return Math.min(1.0, (double) playerCombatLevel / npcCombatLevel);
+ }
+
+ /**
+ * Check if it's safe to engage in combat.
+ */
+ public boolean isSafeToCombat() {
+ // Check health
+ if (shouldEat() && !hasFood()) {
+ logger.warn("Not safe to combat - low health and no food");
+ return false;
+ }
+
+ // Check if already in dangerous combat
+ int currentHp = bridgeAdapter.getSkillLevel(Skill.HITPOINTS);
+ if (currentHp <= 20 && clientCore.getPlayerState().isInCombat()) {
+ logger.warn("Not safe to combat - critical health in combat");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if the player has food in inventory.
+ */
+ public boolean hasFood() {
+ Item[] inventory = bridgeAdapter.getInventory();
+
+ for (Item item : inventory) {
+ if (item != null && !item.isEmpty() && isFood(item.getId())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the amount of food in inventory.
+ */
+ public int getFoodCount() {
+ Item[] inventory = bridgeAdapter.getInventory();
+ int count = 0;
+
+ for (Item item : inventory) {
+ if (item != null && !item.isEmpty() && isFood(item.getId())) {
+ count += item.getQuantity();
+ }
+ }
+
+ return count;
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/EventAPI.java b/modernized-client/src/main/java/com/openosrs/client/api/EventAPI.java
index 9680d30..b1532fc 100644
--- a/modernized-client/src/main/java/com/openosrs/client/api/EventAPI.java
+++ b/modernized-client/src/main/java/com/openosrs/client/api/EventAPI.java
@@ -2,6 +2,14 @@ package com.openosrs.client.api;
import com.openosrs.client.core.ClientCore;
import com.openosrs.client.core.EventSystem;
+import com.openosrs.client.api.types.Position;
+import com.openosrs.client.api.types.InventoryChange;
+import com.openosrs.client.api.types.ExperienceGain;
+import com.openosrs.client.api.types.CombatStateChange;
+import com.openosrs.client.api.types.NPC;
+import com.openosrs.client.api.types.ChatMessage;
+import com.openosrs.client.api.types.HealthInfo;
+import com.openosrs.client.api.types.Skill; // TODO: There is no implementation for Skill
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -9,19 +17,279 @@ import java.util.function.Consumer;
/**
* EventAPI - Provides high-level event registration for agents.
- *
+ *
* This API module provides convenient methods for agents to register
* for various game events without dealing with the low-level event system.
*/
public class EventAPI {
private static final Logger logger = LoggerFactory.getLogger(EventAPI.class);
-
+
private final ClientCore clientCore;
private final EventSystem eventSystem;
-
+
public EventAPI(ClientCore clientCore) {
this.clientCore = clientCore;
this.eventSystem = clientCore.getEventSystem();
}
-
- /**\n * Register a listener for player movement events.\n */\n public void onPlayerMoved(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.PLAYER_MOVED, event -> {\n if (event instanceof EventSystem.PlayerMovedEvent) {\n EventSystem.PlayerMovedEvent moveEvent = (EventSystem.PlayerMovedEvent) event;\n Position position = new Position(moveEvent.getX(), moveEvent.getY(), moveEvent.getPlane());\n listener.accept(position);\n }\n });\n logger.debug(\"Registered player movement listener\");\n }\n \n /**\n * Register a listener for health changes.\n */\n public void onHealthChanged(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.HEALTH_CHANGED, event -> {\n if (event instanceof EventSystem.HealthChangedEvent) {\n EventSystem.HealthChangedEvent healthEvent = (EventSystem.HealthChangedEvent) event;\n HealthInfo healthInfo = new HealthInfo(healthEvent.getCurrent(), healthEvent.getMax());\n listener.accept(healthInfo);\n }\n });\n logger.debug(\"Registered health change listener\");\n }\n \n /**\n * Register a listener for inventory changes.\n */\n public void onInventoryChanged(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.INVENTORY_CHANGED, event -> {\n if (event instanceof EventSystem.InventoryChangedApiEvent) {\n EventSystem.InventoryChangedApiEvent invEvent = (EventSystem.InventoryChangedApiEvent) event;\n listener.accept(invEvent.getChange());\n }\n });\n logger.debug(\"Registered inventory change listener\");\n }\n \n /**\n * Register a listener for experience gains.\n */\n public void onExperienceGained(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.EXPERIENCE_CHANGED, event -> {\n if (event instanceof EventSystem.ExperienceChangedEvent) {\n EventSystem.ExperienceChangedEvent expEvent = (EventSystem.ExperienceChangedEvent) event;\n \n // Convert skill ID to Skill enum\n AgentAPI.Skill skill = null;\n for (AgentAPI.Skill s : AgentAPI.Skill.values()) {\n if (s.getId() == expEvent.getSkill()) {\n skill = s;\n break;\n }\n }\n \n if (skill != null) {\n // We need to track old experience to create ExperienceGain\n // For now, assume 0 as old experience (this should be improved)\n ExperienceGain gain = new ExperienceGain(skill, 0, expEvent.getExperience());\n listener.accept(gain);\n }\n }\n });\n logger.debug(\"Registered experience gain listener\");\n }\n \n /**\n * Register a listener for combat state changes.\n */\n public void onCombatStateChanged(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.COMBAT_STATE_CHANGED, event -> {\n if (event instanceof EventSystem.CombatStateChangedEvent) {\n EventSystem.CombatStateChangedEvent combatEvent = (EventSystem.CombatStateChangedEvent) event;\n \n // TODO: Convert target ID to NPC object when we have NPC tracking\n NPC target = null; // Would need to look up NPC by target ID\n \n CombatStateChange stateChange = new CombatStateChange(combatEvent.isInCombat(), target);\n listener.accept(stateChange);\n }\n });\n logger.debug(\"Registered combat state change listener\");\n }\n \n /**\n * Register a listener for chat messages.\n */\n public void onChatMessage(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.CHAT_MESSAGE, event -> {\n if (event instanceof EventSystem.ChatMessageEvent) {\n EventSystem.ChatMessageEvent chatEvent = (EventSystem.ChatMessageEvent) event;\n ChatMessage message = new ChatMessage(chatEvent.getUsername(), chatEvent.getMessage(), chatEvent.getType());\n listener.accept(message);\n }\n });\n logger.debug(\"Registered chat message listener\");\n }\n \n /**\n * Register a listener for game ticks.\n */\n public void onGameTick(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.GAME_TICK, event -> {\n listener.accept(event.getTimestamp());\n });\n logger.debug(\"Registered game tick listener\");\n }\n \n /**\n * Register a listener for prayer changes.\n */\n public void onPrayerChanged(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.PRAYER_CHANGED, event -> {\n if (event instanceof EventSystem.PrayerChangedEvent) {\n EventSystem.PrayerChangedEvent prayerEvent = (EventSystem.PrayerChangedEvent) event;\n HealthInfo prayerInfo = new HealthInfo(prayerEvent.getCurrent(), prayerEvent.getMax());\n listener.accept(prayerInfo);\n }\n });\n logger.debug(\"Registered prayer change listener\");\n }\n \n /**\n * Register a listener for run energy changes.\n */\n public void onRunEnergyChanged(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.RUN_ENERGY_CHANGED, event -> {\n if (event instanceof EventSystem.RunEnergyChangedEvent) {\n EventSystem.RunEnergyChangedEvent energyEvent = (EventSystem.RunEnergyChangedEvent) event;\n listener.accept(energyEvent.getEnergy());\n }\n });\n logger.debug(\"Registered run energy change listener\");\n }\n \n /**\n * Register a listener for skill level changes.\n */\n public void onSkillChanged(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.SKILL_CHANGED, event -> {\n if (event instanceof EventSystem.SkillChangedEvent) {\n EventSystem.SkillChangedEvent skillEvent = (EventSystem.SkillChangedEvent) event;\n \n // Convert skill ID to Skill enum\n AgentAPI.Skill skill = null;\n for (AgentAPI.Skill s : AgentAPI.Skill.values()) {\n if (s.getId() == skillEvent.getSkill()) {\n skill = s;\n break;\n }\n }\n \n if (skill != null) {\n SkillChange change = new SkillChange(skill, skillEvent.getLevel(), skillEvent.getExperience());\n listener.accept(change);\n }\n }\n });\n logger.debug(\"Registered skill change listener\");\n }\n \n /**\n * Register a listener for animation changes.\n */\n public void onAnimationChanged(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.ANIMATION_CHANGED, event -> {\n if (event instanceof EventSystem.AnimationChangedEvent) {\n EventSystem.AnimationChangedEvent animEvent = (EventSystem.AnimationChangedEvent) event;\n listener.accept(animEvent.getAnimationId());\n }\n });\n logger.debug(\"Registered animation change listener\");\n }\n \n /**\n * Register a listener for interface state changes.\n */\n public void onInterfaceChanged(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.INTERFACE_OPENED, event -> {\n if (event instanceof EventSystem.InterfaceOpenedEvent) {\n EventSystem.InterfaceOpenedEvent interfaceEvent = (EventSystem.InterfaceOpenedEvent) event;\n InterfaceChange change = new InterfaceChange(-1, interfaceEvent.getInterfaceId(), true);\n listener.accept(change);\n }\n });\n \n eventSystem.addListener(EventSystem.EventType.INTERFACE_CLOSED, event -> {\n if (event instanceof EventSystem.InterfaceClosedEvent) {\n EventSystem.InterfaceClosedEvent interfaceEvent = (EventSystem.InterfaceClosedEvent) event;\n InterfaceChange change = new InterfaceChange(interfaceEvent.getInterfaceId(), -1, false);\n listener.accept(change);\n }\n });\n logger.debug(\"Registered interface change listener\");\n }\n \n /**\n * Register a listener for network connection changes.\n */\n public void onConnectionChanged(Consumer listener) {\n eventSystem.addListener(EventSystem.EventType.CONNECTION_LOST, event -> {\n listener.accept(false);\n });\n \n eventSystem.addListener(EventSystem.EventType.CONNECTION_RESTORED, event -> {\n listener.accept(true);\n });\n logger.debug(\"Registered connection change listener\");\n }\n \n /**\n * Helper class for skill changes.\n */\n public static class SkillChange {\n private final AgentAPI.Skill skill;\n private final int level;\n private final int experience;\n \n public SkillChange(AgentAPI.Skill skill, int level, int experience) {\n this.skill = skill;\n this.level = level;\n this.experience = experience;\n }\n \n public AgentAPI.Skill getSkill() { return skill; }\n public int getLevel() { return level; }\n public int getExperience() { return experience; }\n \n @Override\n public String toString() {\n return String.format(\"SkillChange{skill=%s, level=%d, experience=%d}\", skill, level, experience);\n }\n }\n \n /**\n * Helper class for interface changes.\n */\n public static class InterfaceChange {\n private final int oldInterface;\n private final int newInterface;\n private final boolean opened;\n \n public InterfaceChange(int oldInterface, int newInterface, boolean opened) {\n this.oldInterface = oldInterface;\n this.newInterface = newInterface;\n this.opened = opened;\n }\n \n public int getOldInterface() { return oldInterface; }\n public int getNewInterface() { return newInterface; }\n public boolean isOpened() { return opened; }\n \n @Override\n public String toString() {\n return String.format(\"InterfaceChange{old=%d, new=%d, opened=%s}\", oldInterface, newInterface, opened);\n }\n }\n}"
\ No newline at end of file
+
+ /**
+ * Register a listener for player movement events.
+ */
+ public void onPlayerMoved(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.PLAYER_MOVED, event -> {
+ if (event instanceof EventSystem.PlayerMovedEvent) {
+ EventSystem.PlayerMovedEvent moveEvent = (EventSystem.PlayerMovedEvent) event;
+ Position position = new Position(moveEvent.getX(), moveEvent.getY(), moveEvent.getPlane());
+ listener.accept(position);
+ }
+ });
+ logger.debug("Registered player movement listener");
+ }
+
+ /**
+ * Register a listener for health changes.
+ */
+ public void onHealthChanged(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.HEALTH_CHANGED, event -> {
+ if (event instanceof EventSystem.HealthChangedEvent) {
+ EventSystem.HealthChangedEvent healthEvent = (EventSystem.HealthChangedEvent) event;
+ HealthInfo healthInfo = new HealthInfo(healthEvent.getCurrent(), healthEvent.getMax());
+ listener.accept(healthInfo);
+ }
+ });
+ logger.debug("Registered health change listener");
+ }
+
+ /**
+ * Register a listener for inventory changes.
+ */
+ public void onInventoryChanged(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.INVENTORY_CHANGED, event -> {
+ if (event instanceof EventSystem.InventoryChangedApiEvent) {
+ EventSystem.InventoryChangedApiEvent invEvent = (EventSystem.InventoryChangedApiEvent) event;
+ listener.accept(invEvent.getChange());
+ }
+ });
+ logger.debug("Registered inventory change listener");
+ }
+
+ /**
+ * Register a listener for experience gains.
+ */
+ public void onExperienceGained(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.EXPERIENCE_CHANGED, event -> {
+ if (event instanceof EventSystem.ExperienceChangedEvent) {
+ EventSystem.ExperienceChangedEvent expEvent = (EventSystem.ExperienceChangedEvent) event;
+
+ // Convert skill ID to Skill enum
+ Skill skill = null;
+ for (Skill s : Skill.values()) {
+ if (s.getId() == expEvent.getSkill()) {
+ skill = s;
+ break;
+ }
+ }
+
+ if (skill != null) {
+ // We need to track old experience to create ExperienceGain
+ // For now, assume 0 as old experience (this should be improved)
+ ExperienceGain gain = new ExperienceGain(skill, 0, expEvent.getExperience());
+ listener.accept(gain);
+ }
+ }
+ });
+ logger.debug("Registered experience gain listener");
+ }
+
+ /**
+ * Register a listener for combat state changes.
+ */
+ public void onCombatStateChanged(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.COMBAT_STATE_CHANGED, event -> {
+ if (event instanceof EventSystem.CombatStateChangedEvent) {
+ EventSystem.CombatStateChangedEvent combatEvent = (EventSystem.CombatStateChangedEvent) event;
+
+ // TODO: Convert target ID to NPC object when we have NPC tracking
+ NPC target = null; // Would need to look up NPC by target ID
+
+ CombatStateChange stateChange = new CombatStateChange(combatEvent.isInCombat(), target);
+ listener.accept(stateChange);
+ }
+ });
+ logger.debug("Registered combat state change listener");
+ }
+
+ /**
+ * Register a listener for chat messages.
+ */
+ public void onChatMessage(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.CHAT_MESSAGE, event -> {
+ if (event instanceof EventSystem.ChatMessageEvent) {
+ EventSystem.ChatMessageEvent chatEvent = (EventSystem.ChatMessageEvent) event;
+ ChatMessage message = new ChatMessage(chatEvent.getUsername(), chatEvent.getMessage(), chatEvent.getType());
+ listener.accept(message);
+ }
+ });
+ logger.debug("Registered chat message listener");
+ }
+
+ /**
+ * Register a listener for game ticks.
+ */
+ public void onGameTick(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.GAME_TICK, event -> {
+ listener.accept(event.getTimestamp());
+ });
+ logger.debug("Registered game tick listener");
+ }
+
+ /**
+ * Register a listener for prayer changes.
+ */
+ public void onPrayerChanged(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.PRAYER_CHANGED, event -> {
+ if (event instanceof EventSystem.PrayerChangedEvent) {
+ EventSystem.PrayerChangedEvent prayerEvent = (EventSystem.PrayerChangedEvent) event;
+ HealthInfo prayerInfo = new HealthInfo(prayerEvent.getCurrent(), prayerEvent.getMax());
+ listener.accept(prayerInfo);
+ }
+ });
+ logger.debug("Registered prayer change listener");
+ }
+
+ /**
+ * Register a listener for run energy changes.
+ */
+ public void onRunEnergyChanged(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.RUN_ENERGY_CHANGED, event -> {
+ if (event instanceof EventSystem.RunEnergyChangedEvent) {
+ EventSystem.RunEnergyChangedEvent energyEvent = (EventSystem.RunEnergyChangedEvent) event;
+ listener.accept(energyEvent.getEnergy());
+ }
+ });
+ logger.debug("Registered run energy change listener");
+ }
+
+ /**
+ * Register a listener for skill level changes.
+ */
+ public void onSkillChanged(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.SKILL_CHANGED, event -> {
+ if (event instanceof EventSystem.SkillChangedEvent) {
+ EventSystem.SkillChangedEvent skillEvent = (EventSystem.SkillChangedEvent) event;
+
+ // Convert skill ID to Skill enum
+ Skill skill = null;
+ for (Skill s : Skill.values()) {
+ if (s.getId() == skillEvent.getSkill()) {
+ skill = s;
+ break;
+ }
+ }
+
+ if (skill != null) {
+ SkillChange change = new SkillChange(skill, skillEvent.getLevel(), skillEvent.getExperience());
+ listener.accept(change);
+ }
+ }
+ });
+ logger.debug("Registered skill change listener");
+ }
+
+ /**
+ * Register a listener for animation changes.
+ */
+ public void onAnimationChanged(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.ANIMATION_CHANGED, event -> {
+ if (event instanceof EventSystem.AnimationChangedEvent) {
+ EventSystem.AnimationChangedEvent animEvent = (EventSystem.AnimationChangedEvent) event;
+ listener.accept(animEvent.getAnimationId());
+ }
+ });
+ logger.debug("Registered animation change listener");
+ }
+
+ /**
+ * Register a listener for interface state changes.
+ */
+ public void onInterfaceChanged(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.INTERFACE_OPENED, event -> {
+ if (event instanceof EventSystem.InterfaceOpenedEvent) {
+ EventSystem.InterfaceOpenedEvent interfaceEvent = (EventSystem.InterfaceOpenedEvent) event;
+ InterfaceChange change = new InterfaceChange(-1, interfaceEvent.getInterfaceId(), true);
+ listener.accept(change);
+ }
+ });
+
+ eventSystem.addListener(EventSystem.EventType.INTERFACE_CLOSED, event -> {
+ if (event instanceof EventSystem.InterfaceClosedEvent) {
+ EventSystem.InterfaceClosedEvent interfaceEvent = (EventSystem.InterfaceClosedEvent) event;
+ InterfaceChange change = new InterfaceChange(interfaceEvent.getInterfaceId(), -1, false);
+ listener.accept(change);
+ }
+ });
+ logger.debug("Registered interface change listener");
+ }
+
+ /**
+ * Register a listener for network connection changes.
+ */
+ public void onConnectionChanged(Consumer listener) {
+ eventSystem.addListener(EventSystem.EventType.CONNECTION_LOST, event -> {
+ listener.accept(false);
+ });
+
+ eventSystem.addListener(EventSystem.EventType.CONNECTION_RESTORED, event -> {
+ listener.accept(true);
+ });
+ logger.debug("Registered connection change listener");
+ }
+
+ /**
+ * Helper class for skill changes.
+ */
+ public static class SkillChange {
+ private final Skill skill;
+ private final int level;
+ private final int experience;
+
+ public SkillChange(Skill skill, int level, int experience) {
+ this.skill = skill;
+ this.level = level;
+ this.experience = experience;
+ }
+
+ public Skill getSkill() { return skill; } // TODO: There is no implementation for Skill
+ public int getLevel() { return level; }
+ public int getExperience() { return experience; }
+
+ @Override
+ public String toString() {
+ return String.format("SkillChange{skill=%s, level=%d, experience=%d}", skill, level, experience);
+ }
+ }
+
+ /**
+ * Helper class for interface changes.
+ */
+ public static class InterfaceChange {
+ private final int oldInterface;
+ private final int newInterface;
+ private final boolean opened;
+
+ public InterfaceChange(int oldInterface, int newInterface, boolean opened) {
+ this.oldInterface = oldInterface;
+ this.newInterface = newInterface;
+ this.opened = opened;
+ }
+
+ public int getOldInterface() { return oldInterface; }
+ public int getNewInterface() { return newInterface; }
+ public boolean isOpened() { return opened; }
+
+ @Override
+ public String toString() {
+ return String.format("InterfaceChange{old=%d, new=%d, opened=%s}", oldInterface, newInterface, opened);
+ }
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/InteractionAPI.java b/modernized-client/src/main/java/com/openosrs/client/api/InteractionAPI.java
index 617bf94..1c47856 100644
--- a/modernized-client/src/main/java/com/openosrs/client/api/InteractionAPI.java
+++ b/modernized-client/src/main/java/com/openosrs/client/api/InteractionAPI.java
@@ -2,6 +2,11 @@ package com.openosrs.client.api;
import com.openosrs.client.core.ClientCore;
import com.openosrs.client.engine.GameEngine;
+import com.openosrs.client.api.types.Position;
+import com.openosrs.client.api.types.NPC;
+import com.openosrs.client.api.types.GameObject;
+import com.openosrs.client.api.types.Item;
+import com.openosrs.client.api.types.GroundItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -9,7 +14,7 @@ import java.util.concurrent.CompletableFuture;
/**
* InteractionAPI - Handles player interactions with the game world.
- *
+ *
* This API module provides methods for:
* - Movement and pathfinding
* - NPC interactions
@@ -19,10 +24,404 @@ import java.util.concurrent.CompletableFuture;
*/
public class InteractionAPI {
private static final Logger logger = LoggerFactory.getLogger(InteractionAPI.class);
-
+
private final ClientCore clientCore;
private final GameEngine gameEngine;
-
+
public InteractionAPI(ClientCore clientCore, GameEngine gameEngine) {
this.clientCore = clientCore;
- this.gameEngine = gameEngine;\n }\n \n /**\n * Walk to a specific position.\n */\n public CompletableFuture walkTo(Position position) {\n if (position == null) {\n return CompletableFuture.completedFuture(false);\n }\n \n logger.debug(\"Walking to position: {}\", position);\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n // TODO: Integrate with actual game engine walking logic\n // For now, this is a stub that simulates walking\n \n Position currentPos = clientCore.getPlayerState().getPosition();\n if (currentPos == null) {\n logger.warn(\"Cannot walk - player position unknown\");\n return false;\n }\n \n double distance = currentPos.distanceTo(position);\n if (distance == 0) {\n logger.debug(\"Already at target position\");\n return true;\n }\n \n // Simulate walking time based on distance\n int walkTime = (int) (distance * 100); // 100ms per tile roughly\n Thread.sleep(Math.min(walkTime, 5000)); // Cap at 5 seconds\n \n // TODO: Actually send walk packet to server and update player position\n logger.debug(\"Walk completed to {}\", position);\n return true;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"Walk interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error during walk\", e);\n return false;\n }\n });\n }\n \n /**\n * Run to a specific position.\n */\n public CompletableFuture runTo(Position position) {\n if (position == null) {\n return CompletableFuture.completedFuture(false);\n }\n \n logger.debug(\"Running to position: {}\", position);\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n // TODO: Enable run mode if not already enabled\n // Then use walking logic but faster\n \n Position currentPos = clientCore.getPlayerState().getPosition();\n if (currentPos == null) {\n logger.warn(\"Cannot run - player position unknown\");\n return false;\n }\n \n double distance = currentPos.distanceTo(position);\n if (distance == 0) {\n logger.debug(\"Already at target position\");\n return true;\n }\n \n // Simulate running time (faster than walking)\n int runTime = (int) (distance * 60); // 60ms per tile roughly\n Thread.sleep(Math.min(runTime, 3000)); // Cap at 3 seconds\n \n // TODO: Actually send run packet to server and update player position\n logger.debug(\"Run completed to {}\", position);\n return true;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"Run interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error during run\", e);\n return false;\n }\n });\n }\n \n /**\n * Interact with an NPC.\n */\n public CompletableFuture interactWithNPC(NPC npc, String action) {\n if (npc == null || action == null) {\n return CompletableFuture.completedFuture(false);\n }\n \n logger.debug(\"Interacting with NPC {} using action: {}\", npc.getName(), action);\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n // TODO: Check if NPC is in range\n // TODO: Send interaction packet to server\n // TODO: Handle interaction response\n \n Position playerPos = clientCore.getPlayerState().getPosition();\n if (playerPos == null) {\n logger.warn(\"Cannot interact - player position unknown\");\n return false;\n }\n \n double distance = npc.distanceToPlayer(playerPos);\n if (distance > 5) {\n logger.warn(\"NPC too far away for interaction: {} tiles\", distance);\n return false;\n }\n \n // Simulate interaction time\n Thread.sleep(600); // 1 game tick\n \n logger.debug(\"Interaction completed with NPC {}\", npc.getName());\n return true;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"NPC interaction interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error during NPC interaction\", e);\n return false;\n }\n });\n }\n \n /**\n * Interact with a game object.\n */\n public CompletableFuture interactWithObject(GameObject object, String action) {\n if (object == null || action == null) {\n return CompletableFuture.completedFuture(false);\n }\n \n logger.debug(\"Interacting with object {} using action: {}\", object.getName(), action);\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n // TODO: Check if object is in range\n // TODO: Send interaction packet to server\n // TODO: Handle interaction response\n \n Position playerPos = clientCore.getPlayerState().getPosition();\n if (playerPos == null) {\n logger.warn(\"Cannot interact - player position unknown\");\n return false;\n }\n \n if (!object.hasAction(action)) {\n logger.warn(\"Object {} does not have action: {}\", object.getName(), action);\n return false;\n }\n \n double distance = object.distanceToPlayer(playerPos);\n if (distance > 5) {\n logger.warn(\"Object too far away for interaction: {} tiles\", distance);\n return false;\n }\n \n // Simulate interaction time\n Thread.sleep(600); // 1 game tick\n \n logger.debug(\"Interaction completed with object {}\", object.getName());\n return true;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"Object interaction interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error during object interaction\", e);\n return false;\n }\n });\n }\n \n /**\n * Pick up a ground item.\n */\n public CompletableFuture pickupItem(GroundItem item) {\n if (item == null) {\n return CompletableFuture.completedFuture(false);\n }\n \n logger.debug(\"Picking up ground item: {} x{}\", item.getName(), item.getQuantity());\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n // TODO: Check if item is in range\n // TODO: Check if inventory has space\n // TODO: Send pickup packet to server\n \n Position playerPos = clientCore.getPlayerState().getPosition();\n if (playerPos == null) {\n logger.warn(\"Cannot pickup - player position unknown\");\n return false;\n }\n \n double distance = item.distanceToPlayer(playerPos);\n if (distance > 2) {\n logger.warn(\"Item too far away for pickup: {} tiles\", distance);\n return false;\n }\n \n // Check inventory space\n if (clientCore.getInventoryState().isInventoryFull() && !item.getName().equals(\"Coins\")) {\n logger.warn(\"Inventory full - cannot pickup item\");\n return false;\n }\n \n // Simulate pickup time\n Thread.sleep(600); // 1 game tick\n \n logger.debug(\"Pickup completed for item {}\", item.getName());\n return true;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"Item pickup interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error during item pickup\", e);\n return false;\n }\n });\n }\n \n /**\n * Use an inventory item.\n */\n public CompletableFuture useItem(int slot) {\n logger.debug(\"Using item in slot: {}\", slot);\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n // TODO: Validate slot\n // TODO: Send use item packet\n \n Item item = clientCore.getInventoryState().getInventorySlot(slot);\n if (item == null || item.isEmpty()) {\n logger.warn(\"No item in slot {} to use\", slot);\n return false;\n }\n \n // Simulate use time\n Thread.sleep(600); // 1 game tick\n \n logger.debug(\"Used item: {}\", item.getName());\n return true;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"Item use interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error using item\", e);\n return false;\n }\n });\n }\n \n /**\n * Use an item on another item.\n */\n public CompletableFuture useItemOnItem(int sourceSlot, int targetSlot) {\n logger.debug(\"Using item in slot {} on item in slot {}\", sourceSlot, targetSlot);\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n // TODO: Validate both slots\n // TODO: Send use item on item packet\n \n Item sourceItem = clientCore.getInventoryState().getInventorySlot(sourceSlot);\n Item targetItem = clientCore.getInventoryState().getInventorySlot(targetSlot);\n \n if (sourceItem == null || sourceItem.isEmpty()) {\n logger.warn(\"No item in source slot {} to use\", sourceSlot);\n return false;\n }\n \n if (targetItem == null || targetItem.isEmpty()) {\n logger.warn(\"No item in target slot {} to use on\", targetSlot);\n return false;\n }\n \n // Simulate combination time\n Thread.sleep(600); // 1 game tick\n \n logger.debug(\"Used {} on {}\", sourceItem.getName(), targetItem.getName());\n return true;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"Item combination interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error combining items\", e);\n return false;\n }\n });\n }\n \n /**\n * Drop an inventory item.\n */\n public CompletableFuture dropItem(int slot) {\n logger.debug(\"Dropping item in slot: {}\", slot);\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n // TODO: Validate slot\n // TODO: Send drop item packet\n \n Item item = clientCore.getInventoryState().getInventorySlot(slot);\n if (item == null || item.isEmpty()) {\n logger.warn(\"No item in slot {} to drop\", slot);\n return false;\n }\n \n // Simulate drop time\n Thread.sleep(600); // 1 game tick\n \n logger.debug(\"Dropped item: {}\", item.getName());\n return true;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"Item drop interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error dropping item\", e);\n return false;\n }\n });\n }\n \n /**\n * Send a chat message.\n */\n public void sendChatMessage(String message) {\n if (message == null || message.trim().isEmpty()) {\n logger.warn(\"Cannot send empty chat message\");\n return;\n }\n \n logger.debug(\"Sending chat message: {}\", message);\n \n // TODO: Send chat packet to server\n // TODO: Handle chat message validation and formatting\n \n // For now, just log it\n logger.info(\"Chat: {}\", message);\n }\n \n /**\n * Enable or disable run mode.\n */\n public CompletableFuture setRunMode(boolean enabled) {\n logger.debug(\"Setting run mode: {}\", enabled);\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n // TODO: Send run mode packet to server\n // TODO: Update player state\n \n Thread.sleep(100); // Small delay for packet\n \n logger.debug(\"Run mode set to: {}\", enabled);\n return true;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"Run mode change interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error changing run mode\", e);\n return false;\n }\n });\n }\n}"
\ No newline at end of file
+ this.gameEngine = gameEngine;
+ }
+
+ /**
+ * Walk to a specific position.
+ */
+ public CompletableFuture walkTo(Position position) {
+ if (position == null) {
+ return CompletableFuture.completedFuture(false);
+ }
+
+ logger.debug("Walking to position: {}", position);
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // TODO: Integrate with actual game engine walking logic
+ // For now, this is a stub that simulates walking
+
+ Position currentPos = clientCore.getPlayerState().getPosition();
+ if (currentPos == null) {
+ logger.warn("Cannot walk - player position unknown");
+ return false;
+ }
+
+ double distance = currentPos.distanceTo(position);
+ if (distance == 0) {
+ logger.debug("Already at target position");
+ return true;
+ }
+
+ // Simulate walking time based on distance
+ int walkTime = (int) (distance * 100); // 100ms per tile roughly
+ Thread.sleep(Math.min(walkTime, 5000)); // Cap at 5 seconds
+
+ // TODO: Actually send walk packet to server and update player position
+ logger.debug("Walk completed to {}", position);
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("Walk interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error during walk", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Run to a specific position.
+ */
+ public CompletableFuture runTo(Position position) {
+ if (position == null) {
+ return CompletableFuture.completedFuture(false);
+ }
+
+ logger.debug("Running to position: {}", position);
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // TODO: Enable run mode if not already enabled
+ // Then use walking logic but faster
+
+ Position currentPos = clientCore.getPlayerState().getPosition();
+ if (currentPos == null) {
+ logger.warn("Cannot run - player position unknown");
+ return false;
+ }
+
+ double distance = currentPos.distanceTo(position);
+ if (distance == 0) {
+ logger.debug("Already at target position");
+ return true;
+ }
+
+ // Simulate running time (faster than walking)
+ int runTime = (int) (distance * 60); // 60ms per tile roughly
+ Thread.sleep(Math.min(runTime, 3000)); // Cap at 3 seconds
+
+ // TODO: Actually send run packet to server and update player position
+ logger.debug("Run completed to {}", position);
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("Run interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error during run", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Interact with an NPC.
+ */
+ public CompletableFuture interactWithNPC(NPC npc, String action) {
+ if (npc == null || action == null) {
+ return CompletableFuture.completedFuture(false);
+ }
+
+ logger.debug("Interacting with NPC {} using action: {}", npc.getName(), action);
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // TODO: Check if NPC is in range
+ // TODO: Send interaction packet to server
+ // TODO: Handle interaction response
+
+ Position playerPos = clientCore.getPlayerState().getPosition();
+ if (playerPos == null) {
+ logger.warn("Cannot interact - player position unknown");
+ return false;
+ }
+
+ double distance = npc.distanceToPlayer(playerPos);
+ if (distance > 5) {
+ logger.warn("NPC too far away for interaction: {} tiles", distance);
+ return false;
+ }
+
+ // Simulate interaction time
+ Thread.sleep(600); // 1 game tick
+
+ logger.debug("Interaction completed with NPC {}", npc.getName());
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("NPC interaction interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error during NPC interaction", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Interact with a game object.
+ */
+ public CompletableFuture interactWithObject(GameObject object, String action) {
+ if (object == null || action == null) {
+ return CompletableFuture.completedFuture(false);
+ }
+
+ logger.debug("Interacting with object {} using action: {}", object.getName(), action);
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // TODO: Check if object is in range
+ // TODO: Send interaction packet to server
+ // TODO: Handle interaction response
+
+ Position playerPos = clientCore.getPlayerState().getPosition();
+ if (playerPos == null) {
+ logger.warn("Cannot interact - player position unknown");
+ return false;
+ }
+
+ if (!object.hasAction(action)) {
+ logger.warn("Object {} does not have action: {}", object.getName(), action);
+ return false;
+ }
+
+ double distance = object.distanceToPlayer(playerPos);
+ if (distance > 5) {
+ logger.warn("Object too far away for interaction: {} tiles", distance);
+ return false;
+ }
+
+ // Simulate interaction time
+ Thread.sleep(600); // 1 game tick
+
+ logger.debug("Interaction completed with object {}", object.getName());
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("Object interaction interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error during object interaction", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Pick up a ground item.
+ */
+ public CompletableFuture pickupItem(GroundItem item) {
+ if (item == null) {
+ return CompletableFuture.completedFuture(false);
+ }
+
+ logger.debug("Picking up ground item: {} x{}", item.getName(), item.getQuantity());
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // TODO: Check if item is in range
+ // TODO: Check if inventory has space
+ // TODO: Send pickup packet to server
+
+ Position playerPos = clientCore.getPlayerState().getPosition();
+ if (playerPos == null) {
+ logger.warn("Cannot pickup - player position unknown");
+ return false;
+ }
+
+ double distance = item.distanceToPlayer(playerPos);
+ if (distance > 2) {
+ logger.warn("Item too far away for pickup: {} tiles", distance);
+ return false;
+ }
+
+ // Check inventory space
+ if (clientCore.getInventoryState().isInventoryFull() && !item.getName().equals("Coins")) {
+ logger.warn("Inventory full - cannot pickup item");
+ return false;
+ }
+
+ // Simulate pickup time
+ Thread.sleep(600); // 1 game tick
+
+ logger.debug("Pickup completed for item {}", item.getName());
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("Item pickup interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error during item pickup", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Use an inventory item.
+ */
+ public CompletableFuture useItem(int slot) {
+ logger.debug("Using item in slot: {}", slot);
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // TODO: Validate slot
+ // TODO: Send use item packet
+
+ Item item = clientCore.getInventoryState().getInventorySlot(slot);
+ if (item == null || item.isEmpty()) {
+ logger.warn("No item in slot {} to use", slot);
+ return false;
+ }
+
+ // Simulate use time
+ Thread.sleep(600); // 1 game tick
+
+ logger.debug("Used item: {}", item.getName());
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("Item use interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error using item", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Use an item on another item.
+ */
+ public CompletableFuture useItemOnItem(int sourceSlot, int targetSlot) {
+ logger.debug("Using item in slot {} on item in slot {}", sourceSlot, targetSlot);
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // TODO: Validate both slots
+ // TODO: Send use item on item packet
+
+ Item sourceItem = clientCore.getInventoryState().getInventorySlot(sourceSlot);
+ Item targetItem = clientCore.getInventoryState().getInventorySlot(targetSlot);
+
+ if (sourceItem == null || sourceItem.isEmpty()) {
+ logger.warn("No item in source slot {} to use", sourceSlot);
+ return false;
+ }
+
+ if (targetItem == null || targetItem.isEmpty()) {
+ logger.warn("No item in target slot {} to use on", targetSlot);
+ return false;
+ }
+
+ // Simulate combination time
+ Thread.sleep(600); // 1 game tick
+
+ logger.debug("Used {} on {}", sourceItem.getName(), targetItem.getName());
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("Item combination interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error combining items", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Drop an inventory item.
+ */
+ public CompletableFuture dropItem(int slot) {
+ logger.debug("Dropping item in slot: {}", slot);
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // TODO: Validate slot
+ // TODO: Send drop item packet
+
+ Item item = clientCore.getInventoryState().getInventorySlot(slot);
+ if (item == null || item.isEmpty()) {
+ logger.warn("No item in slot {} to drop", slot);
+ return false;
+ }
+
+ // Simulate drop time
+ Thread.sleep(600); // 1 game tick
+
+ logger.debug("Dropped item: {}", item.getName());
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("Item drop interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error dropping item", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Send a chat message.
+ */
+ public void sendChatMessage(String message) {
+ if (message == null || message.trim().isEmpty()) {
+ logger.warn("Cannot send empty chat message");
+ return;
+ }
+
+ logger.debug("Sending chat message: {}", message);
+
+ // TODO: Send chat packet to server
+ // TODO: Handle chat message validation and formatting
+
+ // For now, just log it
+ logger.info("Chat: {}", message);
+ }
+
+ /**
+ * Enable or disable run mode.
+ */
+ public CompletableFuture setRunMode(boolean enabled) {
+ logger.debug("Setting run mode: {}", enabled);
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // TODO: Send run mode packet to server
+ // TODO: Update player state
+
+ Thread.sleep(100); // Small delay for packet
+
+ logger.debug("Run mode set to: {}", enabled);
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("Run mode change interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error changing run mode", e);
+ return false;
+ }
+ });
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/InterfaceAPI.java b/modernized-client/src/main/java/com/openosrs/client/api/InterfaceAPI.java
index c295527..e5d866a 100644
--- a/modernized-client/src/main/java/com/openosrs/client/api/InterfaceAPI.java
+++ b/modernized-client/src/main/java/com/openosrs/client/api/InterfaceAPI.java
@@ -1,43 +1,1039 @@
package com.openosrs.client.api;
-import com.openosrs.client.core.ClientCore;
-import com.openosrs.client.core.InterfaceState;
+import java.util.concurrent.CompletableFuture;
+import java.util.List;
+import java.util.Map;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.concurrent.CompletableFuture;
+import com.openosrs.client.core.ClientCore;
+import com.openosrs.client.core.InterfaceState;
+import com.openosrs.client.api.types.Position;
/**
- * InterfaceAPI - Handles game interface interactions.
- *
+ * Enhanced InterfaceAPI - Comprehensive game interface interactions for agents.
+ *
* This API module provides methods for:
- * - Interface state checking
- * - Interface interaction
- * - Widget manipulation
- * - Dialog handling
+ * - Interface state checking and monitoring
+ * - Widget discovery and interaction
+ * - Dialog handling and text parsing
+ * - Banking and trading automation
+ * - Interface navigation and tab switching
+ * - Text input and button clicking
+ * - Interface element waiting and polling
*/
public class InterfaceAPI {
private static final Logger logger = LoggerFactory.getLogger(InterfaceAPI.class);
-
+
private final ClientCore clientCore;
private final InterfaceState interfaceState;
-
+ private final Map interfaceDefinitions;
+ private final InterfaceInteractionEngine interactionEngine;
+
public InterfaceAPI(ClientCore clientCore) {
this.clientCore = clientCore;
this.interfaceState = clientCore.getInterfaceState();
+ this.interfaceDefinitions = new HashMap<>();
+ this.interactionEngine = new InterfaceInteractionEngine(clientCore);
+
+ initializeInterfaceDefinitions();
}
+ // ================================
+ // BASIC INTERFACE STATE METHODS
+ // ================================
+
/**
* Check if a specific interface is open.
*/
public boolean isInterfaceOpen(int interfaceId) {
return interfaceState.isInterfaceOpen(interfaceId);
}
-
+
/**
* Get the currently open interface ID (-1 if none).
*/
public int getCurrentInterface() {
return interfaceState.getCurrentInterface();
}
- \n /**\n * Check if any interface is open.\n */\n public boolean isAnyInterfaceOpen() {\n return interfaceState.isAnyInterfaceOpen();\n }\n \n /**\n * Close the current interface.\n */\n public CompletableFuture closeInterface() {\n logger.debug(\"Closing current interface\");\n \n return CompletableFuture.supplyAsync(() -> {\n try {\n if (!isAnyInterfaceOpen()) {\n logger.debug(\"No interface open to close\");\n return true;\n }\n \n // TODO: Send close interface packet\n interfaceState.closeInterface();\n \n // Simulate close time\n Thread.sleep(100);\n \n logger.debug(\"Interface closed\");\n return true;\n \n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n logger.warn(\"Interface close interrupted\");\n return false;\n } catch (Exception e) {\n logger.error(\"Error closing interface\", e);\n return false;\n }\n });\n }\n \n /**\n * Check if the login screen is open.\n */\n public boolean isLoginScreenOpen() {\n // TODO: Define login screen interface ID constants\n return isInterfaceOpen(596); // Common login screen ID\n }\n \n /**\n * Check if the game interface (main game) is open.\n */\n public boolean isGameInterfaceOpen() {\n // TODO: Define game interface ID constants\n return isInterfaceOpen(548); // Common game interface ID\n }\n \n /**\n * Check if a dialog is open.\n */\n public boolean isDialogOpen() {\n // TODO: Check for various dialog interface IDs\n return isInterfaceOpen(162) || // NPC dialog\n isInterfaceOpen(243) || // Player dialog\n isInterfaceOpen(217); // Options dialog\n }\n \n /**\n * Check if the chat interface is open.\n */\n public boolean isChatInterfaceOpen() {\n // TODO: Define chat interface ID\n return isInterfaceOpen(162); // Chat interface ID\n }\n \n /**\n * Check if the inventory interface is visible.\n */\n public boolean isInventoryVisible() {\n // TODO: Check if inventory tab is selected\n return true; // Usually always visible in game\n }\n \n /**\n * Check if the skills interface is open.\n */\n public boolean isSkillsInterfaceOpen() {\n // TODO: Define skills interface ID\n return isInterfaceOpen(320); // Skills interface ID\n }\n \n /**\n * Check if the equipment interface is open.\n */\n public boolean isEquipmentInterfaceOpen() {\n // TODO: Define equipment interface ID\n return isInterfaceOpen(387); // Equipment interface ID\n }\n \n /**\n * Check if the prayer interface is open.\n */\n public boolean isPrayerInterfaceOpen() {\n // TODO: Define prayer interface ID\n return isInterfaceOpen(541); // Prayer interface ID\n }\n \n /**\n * Check if the magic interface is open.\n */\n public boolean isMagicInterfaceOpen() {\n // TODO: Define magic interface ID\n return isInterfaceOpen(218); // Magic interface ID\n }\n \n /**\n * Check if a bank interface is open.\n */\n public boolean isBankOpen() {\n // TODO: Define bank interface IDs\n return isInterfaceOpen(12) || // Main bank\n isInterfaceOpen(213); // Deposit box\n }\n \n /**\n * Check if a shop interface is open.\n */\n public boolean isShopOpen() {\n // TODO: Define shop interface ID\n return isInterfaceOpen(300); // Shop interface ID\n }\n \n /**\n * Check if a trade interface is open.\n */\n public boolean isTradeOpen() {\n // TODO: Define trade interface IDs\n return isInterfaceOpen(335) || // Trade screen 1\n isInterfaceOpen(334); // Trade screen 2\n }\n \n /**\n * Check if the Grand Exchange interface is open.\n */\n public boolean isGrandExchangeOpen() {\n // TODO: Define GE interface ID\n return isInterfaceOpen(465); // GE interface ID\n }\n \n /**\n * Wait for a specific interface to open.\n */\n public CompletableFuture waitForInterface(int interfaceId, long timeoutMs) {\n return CompletableFuture.supplyAsync(() -> {\n long startTime = System.currentTimeMillis();\n \n while (!isInterfaceOpen(interfaceId)) {\n if (System.currentTimeMillis() - startTime > timeoutMs) {\n logger.warn(\"Timeout waiting for interface {} to open\", interfaceId);\n return false;\n }\n \n try {\n Thread.sleep(50);\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n return false;\n }\n }\n \n logger.debug(\"Interface {} opened\", interfaceId);\n return true;\n });\n }\n \n /**\n * Wait for any interface to close.\n */\n public CompletableFuture waitForInterfaceClose(long timeoutMs) {\n return CompletableFuture.supplyAsync(() -> {\n long startTime = System.currentTimeMillis();\n \n while (isAnyInterfaceOpen()) {\n if (System.currentTimeMillis() - startTime > timeoutMs) {\n logger.warn(\"Timeout waiting for interface to close\");\n return false;\n }\n \n try {\n Thread.sleep(50);\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n return false;\n }\n }\n \n logger.debug(\"Interface closed\");\n return true;\n });\n }\n \n /**\n * Get interface state information for debugging.\n */\n public String getInterfaceState() {\n int currentInterface = getCurrentInterface();\n StringBuilder state = new StringBuilder();\n \n state.append(\"Current Interface: \").append(currentInterface).append(\"\\n\");\n state.append(\"Login Screen: \").append(isLoginScreenOpen()).append(\"\\n\");\n state.append(\"Game Interface: \").append(isGameInterfaceOpen()).append(\"\\n\");\n state.append(\"Dialog Open: \").append(isDialogOpen()).append(\"\\n\");\n state.append(\"Bank Open: \").append(isBankOpen()).append(\"\\n\");\n state.append(\"Shop Open: \").append(isShopOpen()).append(\"\\n\");\n state.append(\"Trade Open: \").append(isTradeOpen()).append(\"\\n\");\n \n return state.toString();\n }\n}"
\ No newline at end of file
+
+ /**
+ * Check if any interface is open.
+ */
+ public boolean isAnyInterfaceOpen() {
+ return interfaceState.isAnyInterfaceOpen();
+ }
+
+ /**
+ * Close the current interface.
+ */
+ public CompletableFuture closeInterface() {
+ logger.debug("Closing current interface");
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ if (!isAnyInterfaceOpen()) {
+ logger.debug("No interface open to close");
+ return true;
+ }
+
+ // TODO: Send close interface packet
+ interfaceState.closeInterface();
+
+ // Simulate close time
+ Thread.sleep(100);
+
+ logger.debug("Interface closed");
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.warn("Interface close interrupted");
+ return false;
+ } catch (Exception e) {
+ logger.error("Error closing interface", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Check if the login screen is open.
+ */
+ public boolean isLoginScreenOpen() {
+ // TODO: Define login screen interface ID constants
+ return isInterfaceOpen(596); // Common login screen ID
+ }
+
+ /**
+ * Check if the game interface (main game) is open.
+ */
+ public boolean isGameInterfaceOpen() {
+ // TODO: Define game interface ID constants
+ return isInterfaceOpen(548); // Common game interface ID
+ }
+
+ /**
+ * Check if a dialog is open.
+ */
+ public boolean isDialogOpen() {
+ // TODO: Check for various dialog interface IDs
+ return isInterfaceOpen(162) || // NPC dialog
+ isInterfaceOpen(243) || // Player dialog
+ isInterfaceOpen(217); // Options dialog
+ }
+
+ /**
+ * Check if the chat interface is open.
+ */
+ public boolean isChatInterfaceOpen() {
+ // TODO: Define chat interface ID
+ return isInterfaceOpen(162); // Chat interface ID
+ }
+
+ /**
+ * Check if the inventory interface is visible.
+ */
+ public boolean isInventoryVisible() {
+ // TODO: Check if inventory tab is selected
+ return true; // Usually always visible in game
+ }
+
+ /**
+ * Check if the skills interface is open.
+ */
+ public boolean isSkillsInterfaceOpen() {
+ // TODO: Define skills interface ID
+ return isInterfaceOpen(320); // Skills interface ID
+ }
+
+ /**
+ * Check if the equipment interface is open.
+ */
+ public boolean isEquipmentInterfaceOpen() {
+ // TODO: Define equipment interface ID
+ return isInterfaceOpen(387); // Equipment interface ID
+ }
+
+ /**
+ * Check if the prayer interface is open.
+ */
+ public boolean isPrayerInterfaceOpen() {
+ // TODO: Define prayer interface ID
+ return isInterfaceOpen(541); // Prayer interface ID
+ }
+
+ /**
+ * Check if the magic interface is open.
+ */
+ public boolean isMagicInterfaceOpen() {
+ // TODO: Define magic interface ID
+ return isInterfaceOpen(218); // Magic interface ID
+ }
+
+ /**
+ * Check if a bank interface is open.
+ */
+ public boolean isBankOpen() {
+ // TODO: Define bank interface IDs
+ return isInterfaceOpen(12) || // Main bank
+ isInterfaceOpen(213); // Deposit box
+ }
+
+ /**
+ * Check if a shop interface is open.
+ */
+ public boolean isShopOpen() {
+ // TODO: Define shop interface ID
+ return isInterfaceOpen(300); // Shop interface ID
+ }
+
+ /**
+ * Check if a trade interface is open.
+ */
+ public boolean isTradeOpen() {
+ // TODO: Define trade interface IDs
+ return isInterfaceOpen(335) || // Trade screen 1
+ isInterfaceOpen(334); // Trade screen 2
+ }
+
+ /**
+ * Check if the Grand Exchange interface is open.
+ */
+ public boolean isGrandExchangeOpen() {
+ // TODO: Define GE interface ID
+ return isInterfaceOpen(465); // GE interface ID
+ }
+
+ /**
+ * Wait for a specific interface to open.
+ */
+ public CompletableFuture waitForInterface(int interfaceId, long timeoutMs) {
+ return CompletableFuture.supplyAsync(() -> {
+ long startTime = System.currentTimeMillis();
+
+ while (!isInterfaceOpen(interfaceId)) {
+ if (System.currentTimeMillis() - startTime > timeoutMs) {
+ logger.warn("Timeout waiting for interface {} to open", interfaceId);
+ return false;
+ }
+
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ }
+ }
+
+ logger.debug("Interface {} opened", interfaceId);
+ return true;
+ });
+ }
+
+ /**
+ * Wait for any interface to close.
+ */
+ public CompletableFuture waitForInterfaceClose(long timeoutMs) {
+ return CompletableFuture.supplyAsync(() -> {
+ long startTime = System.currentTimeMillis();
+
+ while (isAnyInterfaceOpen()) {
+ if (System.currentTimeMillis() - startTime > timeoutMs) {
+ logger.warn("Timeout waiting for interface to close");
+ return false;
+ }
+
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ }
+ }
+
+ logger.debug("Interface closed");
+ return true;
+ });
+ }
+
+ /**
+ * Get interface state information for debugging.
+ */
+ public String getInterfaceState() {
+ int currentInterface = getCurrentInterface();
+ StringBuilder state = new StringBuilder();
+
+ state.append("Current Interface: ").append(currentInterface).append("\n");
+ state.append("Login Screen: ").append(isLoginScreenOpen()).append("\n");
+ state.append("Game Interface: ").append(isGameInterfaceOpen()).append("\n");
+ state.append("Dialog Open: ").append(isDialogOpen()).append("\n");
+ state.append("Bank Open: ").append(isBankOpen()).append("\n");
+ state.append("Shop Open: ").append(isShopOpen()).append("\n");
+ state.append("Trade Open: ").append(isTradeOpen()).append("\n");
+
+ return state.toString();
+ }
+
+ // ================================
+ // WIDGET DISCOVERY AND INTERACTION
+ // ================================
+
+ /**
+ * Find a widget by text content.
+ */
+ public Optional findWidgetByText(String text) {
+ return findWidgetByText(text, false);
+ }
+
+ /**
+ * Find a widget by text content with case sensitivity option.
+ */
+ public Optional findWidgetByText(String text, boolean caseSensitive) {
+ List allWidgets = getAllVisibleWidgets();
+
+ return allWidgets.stream()
+ .filter(widget -> {
+ String widgetText = widget.getText();
+ if (widgetText == null) return false;
+
+ if (caseSensitive) {
+ return widgetText.contains(text);
+ } else {
+ return widgetText.toLowerCase().contains(text.toLowerCase());
+ }
+ })
+ .findFirst();
+ }
+
+ /**
+ * Find widgets by action (e.g., "Continue", "Close", "Ok").
+ */
+ public List findWidgetsByAction(String action) {
+ List allWidgets = getAllVisibleWidgets();
+
+ return allWidgets.stream()
+ .filter(widget -> {
+ List actions = widget.getActions();
+ return actions != null && actions.contains(action);
+ })
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Find a clickable widget (has actions).
+ */
+ public List getClickableWidgets() {
+ return getAllVisibleWidgets().stream()
+ .filter(widget -> widget.getActions() != null && !widget.getActions().isEmpty())
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Get all visible widgets in current interfaces.
+ */
+ public List getAllVisibleWidgets() {
+ // TODO: Implement widget discovery from RuneLite client
+ return new ArrayList<>(); // Placeholder
+ }
+
+ /**
+ * Click a widget by interface and child ID.
+ */
+ public CompletableFuture clickWidget(int interfaceId, int childId) {
+ return clickWidget(interfaceId, childId, 0);
+ }
+
+ /**
+ * Click a widget with specific action index.
+ */
+ public CompletableFuture clickWidget(int interfaceId, int childId, int actionIndex) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ logger.debug("Clicking widget {}.{} with action index {}", interfaceId, childId, actionIndex);
+
+ // TODO: Implement actual widget clicking through RuneLite bridge
+ boolean success = interactionEngine.clickWidget(interfaceId, childId, actionIndex);
+
+ if (success) {
+ Thread.sleep(100); // Small delay after click
+ logger.debug("Widget click successful");
+ } else {
+ logger.warn("Widget click failed");
+ }
+
+ return success;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ } catch (Exception e) {
+ logger.error("Error clicking widget", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Click a widget by text content.
+ */
+ public CompletableFuture clickWidgetByText(String text) {
+ return CompletableFuture.supplyAsync(() -> {
+ Optional widget = findWidgetByText(text);
+ if (widget.isPresent()) {
+ return clickWidget(widget.get().getInterfaceId(), widget.get().getChildId()).join();
+ } else {
+ logger.warn("Widget with text '{}' not found", text);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Click a widget by action.
+ */
+ public CompletableFuture clickWidgetByAction(String action) {
+ return CompletableFuture.supplyAsync(() -> {
+ List widgets = findWidgetsByAction(action);
+ if (!widgets.isEmpty()) {
+ Widget widget = widgets.get(0); // Use first match
+ int actionIndex = widget.getActions().indexOf(action);
+ return clickWidget(widget.getInterfaceId(), widget.getChildId(), actionIndex).join();
+ } else {
+ logger.warn("Widget with action '{}' not found", action);
+ return false;
+ }
+ });
+ }
+
+ // ================================
+ // DIALOG HANDLING
+ // ================================
+
+ /**
+ * Get current dialog text.
+ */
+ public Optional getDialogText() {
+ if (!isDialogOpen()) {
+ return Optional.empty();
+ }
+
+ // TODO: Extract dialog text from interface widgets
+ return Optional.of("Dialog text placeholder");
+ }
+
+ /**
+ * Get available dialog options.
+ */
+ public List getDialogOptions() {
+ if (!isDialogOpen()) {
+ return new ArrayList<>();
+ }
+
+ // TODO: Extract dialog options from interface widgets
+ return new ArrayList<>(); // Placeholder
+ }
+
+ /**
+ * Continue dialog by clicking "Continue" button.
+ */
+ public CompletableFuture continueDialog() {
+ return clickWidgetByAction("Continue");
+ }
+
+ /**
+ * Select a dialog option by text.
+ */
+ public CompletableFuture selectDialogOption(String optionText) {
+ return clickWidgetByText(optionText);
+ }
+
+ /**
+ * Select a dialog option by index (1-based).
+ */
+ public CompletableFuture selectDialogOption(int optionIndex) {
+ List options = getDialogOptions();
+ if (optionIndex > 0 && optionIndex <= options.size()) {
+ return selectDialogOption(options.get(optionIndex - 1));
+ } else {
+ logger.warn("Invalid dialog option index: {}", optionIndex);
+ return CompletableFuture.completedFuture(false);
+ }
+ }
+
+ /**
+ * Wait for dialog to appear and handle it automatically.
+ */
+ public CompletableFuture handleDialog(String expectedText, String response) {
+ return CompletableFuture.supplyAsync(() -> {
+ // Wait for dialog
+ long startTime = System.currentTimeMillis();
+ while (!isDialogOpen() && (System.currentTimeMillis() - startTime) < 5000) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ }
+ }
+
+ if (!isDialogOpen()) {
+ logger.warn("Dialog did not appear within timeout");
+ return false;
+ }
+
+ // Check dialog text if specified
+ if (expectedText != null) {
+ Optional dialogText = getDialogText();
+ if (dialogText.isEmpty() || !dialogText.get().contains(expectedText)) {
+ logger.warn("Dialog text does not match expected: {}", expectedText);
+ return false;
+ }
+ }
+
+ // Respond
+ if (response.equalsIgnoreCase("continue")) {
+ return continueDialog().join();
+ } else {
+ return selectDialogOption(response).join();
+ }
+ });
+ }
+
+ // ================================
+ // BANKING INTERFACE
+ // ================================
+
+ /**
+ * Deposit all items in inventory.
+ */
+ public CompletableFuture depositAll() {
+ if (!isBankOpen()) {
+ logger.warn("Bank is not open");
+ return CompletableFuture.completedFuture(false);
+ }
+
+ return clickWidgetByText("Deposit inventory");
+ }
+
+ /**
+ * Deposit all equipment.
+ */
+ public CompletableFuture depositEquipment() {
+ if (!isBankOpen()) {
+ logger.warn("Bank is not open");
+ return CompletableFuture.completedFuture(false);
+ }
+
+ return clickWidgetByText("Deposit equipment");
+ }
+
+ /**
+ * Withdraw item by name and quantity.
+ */
+ public CompletableFuture withdrawItem(String itemName, int quantity) {
+ if (!isBankOpen()) {
+ logger.warn("Bank is not open");
+ return CompletableFuture.completedFuture(false);
+ }
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // TODO: Implement bank item search and withdrawal
+ logger.debug("Withdrawing {} x {}", quantity, itemName);
+
+ // Set withdraw quantity
+ setWithdrawQuantity(quantity);
+
+ // Find and click item
+ Optional itemWidget = findBankItem(itemName);
+ if (itemWidget.isPresent()) {
+ return clickWidget(itemWidget.get().getInterfaceId(), itemWidget.get().getChildId()).join();
+ } else {
+ logger.warn("Item '{}' not found in bank", itemName);
+ return false;
+ }
+
+ } catch (Exception e) {
+ logger.error("Error withdrawing item", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Set bank withdrawal quantity.
+ */
+ public CompletableFuture setWithdrawQuantity(int quantity) {
+ String quantityText;
+ switch (quantity) {
+ case 1:
+ quantityText = "1";
+ break;
+ case 5:
+ quantityText = "5";
+ break;
+ case 10:
+ quantityText = "10";
+ break;
+ case -1:
+ quantityText = "All";
+ break;
+ default:
+ quantityText = "X";
+ break;
+ }
+
+ return clickWidgetByText(quantityText).thenCompose(success -> {
+ if (success && quantityText.equals("X")) {
+ // TODO: Handle entering custom quantity
+ return enterText(String.valueOf(quantity));
+ }
+ return CompletableFuture.completedFuture(success);
+ });
+ }
+
+ /**
+ * Find an item in the bank interface.
+ */
+ private Optional findBankItem(String itemName) {
+ // TODO: Implement bank item search
+ return Optional.empty(); // Placeholder
+ }
+
+ // ================================
+ // TEXT INPUT HANDLING
+ // ================================
+
+ /**
+ * Enter text in the currently focused input field.
+ */
+ public CompletableFuture enterText(String text) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ logger.debug("Entering text: {}", text);
+
+ // TODO: Implement text input through RuneLite bridge
+ boolean success = interactionEngine.typeText(text);
+
+ if (success) {
+ Thread.sleep(200); // Delay for text input
+ logger.debug("Text input successful");
+ } else {
+ logger.warn("Text input failed");
+ }
+
+ return success;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ } catch (Exception e) {
+ logger.error("Error entering text", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Press Enter key.
+ */
+ public CompletableFuture pressEnter() {
+ return interactionEngine.pressKey("ENTER");
+ }
+
+ /**
+ * Press Escape key.
+ */
+ public CompletableFuture pressEscape() {
+ return interactionEngine.pressKey("ESCAPE");
+ }
+
+ // ================================
+ // TAB SWITCHING
+ // ================================
+
+ /**
+ * Switch to a game tab.
+ */
+ public CompletableFuture switchToTab(GameTab tab) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ logger.debug("Switching to tab: {}", tab);
+
+ // TODO: Implement tab switching through RuneLite bridge
+ boolean success = interactionEngine.clickTab(tab);
+
+ if (success) {
+ Thread.sleep(100); // Delay for tab switch
+ logger.debug("Tab switch successful");
+ } else {
+ logger.warn("Tab switch failed");
+ }
+
+ return success;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ } catch (Exception e) {
+ logger.error("Error switching tab", e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Get the currently active tab.
+ */
+ public GameTab getCurrentTab() {
+ // TODO: Implement current tab detection
+ return GameTab.INVENTORY; // Placeholder
+ }
+
+ // ================================
+ // ADVANCED WAITING AND POLLING
+ // ================================
+
+ /**
+ * Wait for a widget to appear with custom condition.
+ */
+ public CompletableFuture> waitForWidget(Predicate condition, long timeoutMs) {
+ return CompletableFuture.supplyAsync(() -> {
+ long startTime = System.currentTimeMillis();
+
+ while (System.currentTimeMillis() - startTime < timeoutMs) {
+ List widgets = getAllVisibleWidgets();
+ Optional match = widgets.stream()
+ .filter(condition)
+ .findFirst();
+
+ if (match.isPresent()) {
+ return match;
+ }
+
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return Optional.empty();
+ }
+ }
+
+ return Optional.empty();
+ });
+ }
+
+ /**
+ * Wait for interface state change.
+ */
+ public CompletableFuture waitForInterfaceChange(long timeoutMs) {
+ final int initialInterface = getCurrentInterface();
+
+ return CompletableFuture.supplyAsync(() -> {
+ long startTime = System.currentTimeMillis();
+
+ while (System.currentTimeMillis() - startTime < timeoutMs) {
+ if (getCurrentInterface() != initialInterface) {
+ return true;
+ }
+
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ }
+ }
+
+ return false;
+ });
+ }
+
+ // ================================
+ // HELPER METHODS
+ // ================================
+
+ /**
+ * Initialize interface definitions and constants.
+ */
+ private void initializeInterfaceDefinitions() {
+ // Login interfaces
+ interfaceDefinitions.put("LOGIN_MAIN", new InterfaceDefinition(596, "Main login screen"));
+ interfaceDefinitions.put("LOGIN_EXISTING", new InterfaceDefinition(378, "Existing user login"));
+
+ // Game interfaces
+ interfaceDefinitions.put("GAME_MAIN", new InterfaceDefinition(548, "Main game interface"));
+ interfaceDefinitions.put("CHATBOX", new InterfaceDefinition(162, "Chat interface"));
+
+ // Dialog interfaces
+ interfaceDefinitions.put("NPC_DIALOG", new InterfaceDefinition(231, "NPC dialog"));
+ interfaceDefinitions.put("PLAYER_DIALOG", new InterfaceDefinition(217, "Player dialog"));
+ interfaceDefinitions.put("OPTIONS_DIALOG", new InterfaceDefinition(219, "Options dialog"));
+
+ // Skill interfaces
+ interfaceDefinitions.put("SKILLS", new InterfaceDefinition(320, "Skills interface"));
+ interfaceDefinitions.put("EQUIPMENT", new InterfaceDefinition(387, "Equipment interface"));
+ interfaceDefinitions.put("PRAYER", new InterfaceDefinition(541, "Prayer interface"));
+ interfaceDefinitions.put("MAGIC", new InterfaceDefinition(218, "Magic interface"));
+
+ // Trading and banking
+ interfaceDefinitions.put("BANK", new InterfaceDefinition(12, "Bank interface"));
+ interfaceDefinitions.put("DEPOSIT_BOX", new InterfaceDefinition(192, "Deposit box"));
+ interfaceDefinitions.put("SHOP", new InterfaceDefinition(300, "Shop interface"));
+ interfaceDefinitions.put("TRADE_FIRST", new InterfaceDefinition(335, "Trade screen 1"));
+ interfaceDefinitions.put("TRADE_SECOND", new InterfaceDefinition(334, "Trade screen 2"));
+ interfaceDefinitions.put("GRAND_EXCHANGE", new InterfaceDefinition(465, "Grand Exchange"));
+
+ logger.debug("Initialized {} interface definitions", interfaceDefinitions.size());
+ }
+
+ /**
+ * Get interface definition by name.
+ */
+ public Optional getInterfaceDefinition(String name) {
+ return Optional.ofNullable(interfaceDefinitions.get(name));
+ }
+}
+
+/**
+ * Widget representation for interface elements.
+ */
+class Widget {
+ private final int interfaceId;
+ private final int childId;
+ private final String text;
+ private final List actions;
+ private final Position position;
+ private final int width;
+ private final int height;
+ private final boolean visible;
+ private final boolean clickable;
+
+ public Widget(int interfaceId, int childId, String text, List actions,
+ Position position, int width, int height, boolean visible, boolean clickable) {
+ this.interfaceId = interfaceId;
+ this.childId = childId;
+ this.text = text;
+ this.actions = actions != null ? new ArrayList<>(actions) : new ArrayList<>();
+ this.position = position;
+ this.width = width;
+ this.height = height;
+ this.visible = visible;
+ this.clickable = clickable;
+ }
+
+ public int getInterfaceId() { return interfaceId; }
+ public int getChildId() { return childId; }
+ public String getText() { return text; }
+ public List getActions() { return new ArrayList<>(actions); }
+ public Position getPosition() { return position; }
+ public int getWidth() { return width; }
+ public int getHeight() { return height; }
+ public boolean isVisible() { return visible; }
+ public boolean isClickable() { return clickable; }
+
+ public boolean hasAction(String action) {
+ return actions.contains(action);
+ }
+
+ public boolean containsText(String text) {
+ return this.text != null && this.text.toLowerCase().contains(text.toLowerCase());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Widget[%d.%d: '%s', actions=%s, pos=%s, visible=%s]",
+ interfaceId, childId, text, actions, position, visible);
+ }
+}
+
+/**
+ * Interface definition for organizing interface constants.
+ */
+class InterfaceDefinition {
+ private final int interfaceId;
+ private final String description;
+ private final Map childIds;
+
+ public InterfaceDefinition(int interfaceId, String description) {
+ this.interfaceId = interfaceId;
+ this.description = description;
+ this.childIds = new HashMap<>();
+ }
+
+ public int getInterfaceId() { return interfaceId; }
+ public String getDescription() { return description; }
+
+ public InterfaceDefinition addChild(String name, int childId) {
+ childIds.put(name, childId);
+ return this;
+ }
+
+ public Optional getChildId(String name) {
+ return Optional.ofNullable(childIds.get(name));
+ }
+
+ public Map getAllChildren() {
+ return new HashMap<>(childIds);
+ }
+}
+
+/**
+ * Game tab enumeration.
+ */
+enum GameTab {
+ COMBAT("Combat", 0),
+ SKILLS("Skills", 1),
+ QUEST("Quest", 2),
+ INVENTORY("Inventory", 3),
+ EQUIPMENT("Equipment", 4),
+ PRAYER("Prayer", 5),
+ MAGIC("Magic", 6),
+ CLAN_CHAT("Clan Chat", 7),
+ FRIENDS("Friends", 8),
+ IGNORE("Ignore", 9),
+ LOGOUT("Logout", 10),
+ OPTIONS("Options", 11),
+ EMOTES("Emotes", 12),
+ MUSIC("Music", 13);
+
+ private final String name;
+ private final int index;
+
+ GameTab(String name, int index) {
+ this.name = name;
+ this.index = index;
+ }
+
+ public String getName() { return name; }
+ public int getIndex() { return index; }
+
+ @Override
+ public String toString() { return name; }
+}
+
+/**
+ * Interface interaction engine for low-level operations.
+ */
+class InterfaceInteractionEngine {
+ private static final Logger logger = LoggerFactory.getLogger(InterfaceInteractionEngine.class);
+
+ private final ClientCore clientCore;
+
+ public InterfaceInteractionEngine(ClientCore clientCore) {
+ this.clientCore = clientCore;
+ }
+
+ /**
+ * Click a widget through the RuneLite bridge.
+ */
+ public boolean clickWidget(int interfaceId, int childId, int actionIndex) {
+ try {
+ logger.debug("Executing widget click: {}.{} action {}", interfaceId, childId, actionIndex);
+
+ // TODO: Implement actual widget clicking through RuneLite bridge
+ // This would involve:
+ // 1. Finding the widget in the RuneLite client
+ // 2. Sending the appropriate click event
+ // 3. Handling the response
+
+ // Placeholder implementation
+ return true;
+
+ } catch (Exception e) {
+ logger.error("Failed to click widget {}.{}", interfaceId, childId, e);
+ return false;
+ }
+ }
+
+ /**
+ * Type text through the RuneLite bridge.
+ */
+ public boolean typeText(String text) {
+ try {
+ logger.debug("Typing text: {}", text);
+
+ // TODO: Implement text input through RuneLite bridge
+ // This would involve sending keyboard events to the client
+
+ // Placeholder implementation
+ return true;
+
+ } catch (Exception e) {
+ logger.error("Failed to type text: {}", text, e);
+ return false;
+ }
+ }
+
+ /**
+ * Press a key through the RuneLite bridge.
+ */
+ public CompletableFuture pressKey(String key) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ logger.debug("Pressing key: {}", key);
+
+ // TODO: Implement key press through RuneLite bridge
+ // This would involve sending specific key events
+
+ Thread.sleep(50); // Small delay for key press
+ return true;
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ } catch (Exception e) {
+ logger.error("Failed to press key: {}", key, e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Click a game tab through the RuneLite bridge.
+ */
+ public boolean clickTab(GameTab tab) {
+ try {
+ logger.debug("Clicking tab: {}", tab.getName());
+
+ // TODO: Implement tab clicking through RuneLite bridge
+ // This would involve finding the tab widget and clicking it
+
+ // Placeholder implementation
+ return true;
+
+ } catch (Exception e) {
+ logger.error("Failed to click tab: {}", tab.getName(), e);
+ return false;
+ }
+ }
+
+ /**
+ * Get widget information from the RuneLite client.
+ */
+ public List getWidgets(int interfaceId) {
+ try {
+ logger.debug("Getting widgets for interface: {}", interfaceId);
+
+ // TODO: Implement widget retrieval through RuneLite bridge
+ // This would involve:
+ // 1. Accessing the RuneLite client's widget system
+ // 2. Extracting widget information
+ // 3. Converting to our Widget objects
+
+ // Placeholder implementation
+ return new ArrayList<>();
+
+ } catch (Exception e) {
+ logger.error("Failed to get widgets for interface: {}", interfaceId, e);
+ return new ArrayList<>();
+ }
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/InventoryAPI.java b/modernized-client/src/main/java/com/openosrs/client/api/InventoryAPI.java
index 3cc96f7..d921c4c 100644
--- a/modernized-client/src/main/java/com/openosrs/client/api/InventoryAPI.java
+++ b/modernized-client/src/main/java/com/openosrs/client/api/InventoryAPI.java
@@ -1,8 +1,9 @@
package com.openosrs.client.api;
import com.openosrs.client.core.ClientCore;
-import com.openosrs.client.core.state.InventoryState;
+import com.openosrs.client.core.InventoryState;
import com.openosrs.client.core.bridge.BridgeAdapter;
+import com.openosrs.client.api.types.Item;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -12,7 +13,7 @@ import java.util.stream.Collectors;
/**
* InventoryAPI - Provides access to inventory and equipment state.
- *
+ *
* This API module handles:
* - Inventory management (28 slots)
* - Equipment management (14 slots)
@@ -21,26 +22,26 @@ import java.util.stream.Collectors;
*/
public class InventoryAPI {
private static final Logger logger = LoggerFactory.getLogger(InventoryAPI.class);
-
+
private final ClientCore clientCore;
private final InventoryState inventoryState;
private final BridgeAdapter bridgeAdapter;
-
+
public InventoryAPI(ClientCore clientCore, BridgeAdapter bridgeAdapter) {
this.clientCore = clientCore;
this.inventoryState = clientCore.getInventoryState();
this.bridgeAdapter = bridgeAdapter;
}
-
+
// === INVENTORY METHODS ===
-
+
/**
* Get the entire inventory as an array.
*/
public Item[] getInventory() {
return bridgeAdapter.getInventory();
}
-
+
/**
* Get an item from a specific inventory slot (0-27).
*/
@@ -48,11 +49,11 @@ public class InventoryAPI {
if (slot < 0 || slot >= 28) {
return Item.EMPTY;
}
-
+
Item[] inventory = getInventory();
return inventory[slot];
}
-
+
/**
* Check if the inventory contains an item with the specified name.
*/
@@ -60,7 +61,7 @@ public class InventoryAPI {
return Arrays.stream(getInventory())
.anyMatch(item -> !item.isEmpty() && item.getName().equalsIgnoreCase(name));
}
-
+
/**
* Check if the inventory contains an item with the specified ID.
*/
@@ -68,7 +69,7 @@ public class InventoryAPI {
return Arrays.stream(getInventory())
.anyMatch(item -> !item.isEmpty() && item.getId() == id);
}
-
+
/**
* Get the total count of items with the specified name.
*/
@@ -78,7 +79,7 @@ public class InventoryAPI {
.mapToInt(Item::getQuantity)
.sum();
}
-
+
/**
* Get the total count of items with the specified ID.
*/
@@ -88,7 +89,7 @@ public class InventoryAPI {
.mapToInt(Item::getQuantity)
.sum();
}
-
+
/**
* Get all items in the inventory with the specified name.
*/
@@ -97,7 +98,7 @@ public class InventoryAPI {
.filter(item -> !item.isEmpty() && item.getName().equalsIgnoreCase(name))
.collect(Collectors.toList());
}
-
+
/**
* Get all items in the inventory with the specified ID.
*/
@@ -106,7 +107,7 @@ public class InventoryAPI {
.filter(item -> !item.isEmpty() && item.getId() == id)
.collect(Collectors.toList());
}
-
+
/**
* Get the first item in the inventory with the specified name.
*/
@@ -116,7 +117,7 @@ public class InventoryAPI {
.findFirst()
.orElse(Item.EMPTY);
}
-
+
/**
* Get the first item in the inventory with the specified ID.
*/
@@ -126,9 +127,271 @@ public class InventoryAPI {
.findFirst()
.orElse(Item.EMPTY);
}
-
+
/**
* Get the slot index of the first item with the specified name (-1 if not found).
*/
public int getItemSlot(String name) {
- Item[] inventory = getInventory();\n for (int i = 0; i < inventory.length; i++) {\n Item item = inventory[i];\n if (!item.isEmpty() && item.getName().equalsIgnoreCase(name)) {\n return i;\n }\n }\n return -1;\n }\n \n /**\n * Get the slot index of the first item with the specified ID (-1 if not found).\n */\n public int getItemSlot(int id) {\n Item[] inventory = getInventory();\n for (int i = 0; i < inventory.length; i++) {\n Item item = inventory[i];\n if (!item.isEmpty() && item.getId() == id) {\n return i;\n }\n }\n return -1;\n }\n \n /**\n * Check if the inventory is full.\n */\n public boolean isFull() {\n return Arrays.stream(getInventory())\n .noneMatch(Item::isEmpty);\n }\n \n /**\n * Get the number of free inventory slots.\n */\n public int getFreeSlots() {\n return (int) Arrays.stream(getInventory())\n .filter(Item::isEmpty)\n .count();\n }\n \n /**\n * Check if the inventory can fit an item (considering stackability).\n */\n public boolean canFitItem(int itemId, int quantity) {\n // If we have the item and it's stackable, we can always fit more\n if (hasItem(itemId)) {\n Item existingItem = getFirstItem(itemId);\n if (existingItem.isStackable()) {\n return true;\n }\n }\n \n // Otherwise, we need free slots\n return getFreeSlots() > 0;\n }\n \n /**\n * Check if the inventory can fit an item by name.\n */\n public boolean canFitItem(String name) {\n // If we have the item and it's stackable, we can always fit more\n if (hasItem(name)) {\n Item existingItem = getFirstItem(name);\n if (existingItem.isStackable()) {\n return true;\n }\n }\n \n // Otherwise, we need free slots\n return getFreeSlots() > 0;\n }\n \n // === EQUIPMENT METHODS ===\n \n /**\n * Get the entire equipment as an array.\n */\n public Item[] getEquipment() {\n return bridgeAdapter.getEquipment();\n }\n \n /**\n * Get an item from a specific equipment slot.\n */\n public Item getEquipmentItem(EquipmentSlot slot) {\n Item[] equipment = getEquipment();\n int slotIndex = slot.getSlotIndex();\n \n if (slotIndex < 0 || slotIndex >= equipment.length) {\n return Item.EMPTY;\n }\n \n return equipment[slotIndex];\n }\n \n /**\n * Check if a specific equipment slot is occupied.\n */\n public boolean isEquipped(EquipmentSlot slot) {\n return !getEquipmentItem(slot).isEmpty();\n }\n \n /**\n * Check if an item with the specified name is equipped.\n */\n public boolean isEquipped(String name) {\n return Arrays.stream(getEquipment())\n .anyMatch(item -> !item.isEmpty() && item.getName().equalsIgnoreCase(name));\n }\n \n /**\n * Check if an item with the specified ID is equipped.\n */\n public boolean isEquipped(int id) {\n return Arrays.stream(getEquipment())\n .anyMatch(item -> !item.isEmpty() && item.getId() == id);\n }\n \n /**\n * Get the equipped weapon.\n */\n public Item getWeapon() {\n return getEquipmentItem(EquipmentSlot.WEAPON);\n }\n \n /**\n * Get the equipped shield.\n */\n public Item getShield() {\n return getEquipmentItem(EquipmentSlot.SHIELD);\n }\n \n /**\n * Get the equipped helmet.\n */\n public Item getHelmet() {\n return getEquipmentItem(EquipmentSlot.HEAD);\n }\n \n /**\n * Get the equipped chestplate/body armor.\n */\n public Item getChestplate() {\n return getEquipmentItem(EquipmentSlot.BODY);\n }\n \n /**\n * Get the equipped legs.\n */\n public Item getLegs() {\n return getEquipmentItem(EquipmentSlot.LEGS);\n }\n \n /**\n * Get the equipped boots.\n */\n public Item getBoots() {\n return getEquipmentItem(EquipmentSlot.FEET);\n }\n \n /**\n * Get the equipped gloves.\n */\n public Item getGloves() {\n return getEquipmentItem(EquipmentSlot.HANDS);\n }\n \n /**\n * Get the equipped cape.\n */\n public Item getCape() {\n return getEquipmentItem(EquipmentSlot.CAPE);\n }\n \n /**\n * Get the equipped amulet.\n */\n public Item getAmulet() {\n return getEquipmentItem(EquipmentSlot.AMULET);\n }\n \n /**\n * Get the equipped ring.\n */\n public Item getRing() {\n return getEquipmentItem(EquipmentSlot.RING);\n }\n \n /**\n * Get the equipped arrows.\n */\n public Item getArrows() {\n return getEquipmentItem(EquipmentSlot.AMMO);\n }\n \n // === UTILITY METHODS ===\n \n /**\n * Get all non-empty items in the inventory.\n */\n public List- getAllItems() {\n return Arrays.stream(getInventory())\n .filter(item -> !item.isEmpty())\n .collect(Collectors.toList());\n }\n \n /**\n * Get all equipped items.\n */\n public List
- getAllEquippedItems() {\n return Arrays.stream(getEquipment())\n .filter(item -> !item.isEmpty())\n .collect(Collectors.toList());\n }\n \n /**\n * Check if the inventory contains food (items with \"eat\" action).\n */\n public boolean hasFood() {\n return Arrays.stream(getInventory())\n .filter(item -> !item.isEmpty())\n .anyMatch(item -> Arrays.asList(item.getActions()).contains(\"Eat\"));\n }\n \n /**\n * Get the first food item in the inventory.\n */\n public Item getFirstFood() {\n return Arrays.stream(getInventory())\n .filter(item -> !item.isEmpty())\n .filter(item -> Arrays.asList(item.getActions()).contains(\"Eat\"))\n .findFirst()\n .orElse(Item.EMPTY);\n }\n}\n\n/**\n * Equipment slot enumeration.\n */\nenum EquipmentSlot {\n HEAD(0),\n CAPE(1),\n AMULET(2),\n WEAPON(3),\n BODY(4),\n SHIELD(5),\n LEGS(7),\n HANDS(9),\n FEET(10),\n RING(12),\n AMMO(13);\n \n private final int slotIndex;\n \n EquipmentSlot(int slotIndex) {\n this.slotIndex = slotIndex;\n }\n \n public int getSlotIndex() {\n return slotIndex;\n }\n}"
\ No newline at end of file
+ Item[] inventory = getInventory();
+ for (int i = 0; i < inventory.length; i++) {
+ Item item = inventory[i];
+ if (!item.isEmpty() && item.getName().equalsIgnoreCase(name)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Get the slot index of the first item with the specified ID (-1 if not found).
+ */
+ public int getItemSlot(int id) {
+ Item[] inventory = getInventory();
+ for (int i = 0; i < inventory.length; i++) {
+ Item item = inventory[i];
+ if (!item.isEmpty() && item.getId() == id) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Check if the inventory is full.
+ */
+ public boolean isFull() {
+ return Arrays.stream(getInventory())
+ .noneMatch(Item::isEmpty);
+ }
+
+ /**
+ * Get the number of free inventory slots.
+ */
+ public int getFreeSlots() {
+ return (int) Arrays.stream(getInventory())
+ .filter(Item::isEmpty)
+ .count();
+ }
+
+ /**
+ * Check if the inventory can fit an item (considering stackability).
+ */
+ public boolean canFitItem(int itemId, int quantity) {
+ // If we have the item and it's stackable, we can always fit more
+ if (hasItem(itemId)) {
+ Item existingItem = getFirstItem(itemId);
+ if (existingItem.isStackable()) {
+ return true;
+ }
+ }
+
+ // Otherwise, we need free slots
+ return getFreeSlots() > 0;
+ }
+
+ /**
+ * Check if the inventory can fit an item by name.
+ */
+ public boolean canFitItem(String name) {
+ // If we have the item and it's stackable, we can always fit more
+ if (hasItem(name)) {
+ Item existingItem = getFirstItem(name);
+ if (existingItem.isStackable()) {
+ return true;
+ }
+ }
+
+ // Otherwise, we need free slots
+ return getFreeSlots() > 0;
+ }
+
+ // === EQUIPMENT METHODS ===
+
+ /**
+ * Get the entire equipment as an array.
+ */
+ public Item[] getEquipment() {
+ return bridgeAdapter.getEquipment();
+ }
+
+ /**
+ * Get an item from a specific equipment slot.
+ */
+ public Item getEquipmentItem(EquipmentSlot slot) {
+ Item[] equipment = getEquipment();
+ int slotIndex = slot.getSlotIndex();
+
+ if (slotIndex < 0 || slotIndex >= equipment.length) {
+ return Item.EMPTY;
+ }
+
+ return equipment[slotIndex];
+ }
+
+ /**
+ * Check if a specific equipment slot is occupied.
+ */
+ public boolean isEquipped(EquipmentSlot slot) {
+ return !getEquipmentItem(slot).isEmpty();
+ }
+
+ /**
+ * Check if an item with the specified name is equipped.
+ */
+ public boolean isEquipped(String name) {
+ return Arrays.stream(getEquipment())
+ .anyMatch(item -> !item.isEmpty() && item.getName().equalsIgnoreCase(name));
+ }
+
+ /**
+ * Check if an item with the specified ID is equipped.
+ */
+ public boolean isEquipped(int id) {
+ return Arrays.stream(getEquipment())
+ .anyMatch(item -> !item.isEmpty() && item.getId() == id);
+ }
+
+ /**
+ * Get the equipped weapon.
+ */
+ public Item getWeapon() {
+ return getEquipmentItem(EquipmentSlot.WEAPON);
+ }
+
+ /**
+ * Get the equipped shield.
+ */
+ public Item getShield() {
+ return getEquipmentItem(EquipmentSlot.SHIELD);
+ }
+
+ /**
+ * Get the equipped helmet.
+ */
+ public Item getHelmet() {
+ return getEquipmentItem(EquipmentSlot.HEAD);
+ }
+
+ /**
+ * Get the equipped chestplate/body armor.
+ */
+ public Item getChestplate() {
+ return getEquipmentItem(EquipmentSlot.BODY);
+ }
+
+ /**
+ * Get the equipped legs.
+ */
+ public Item getLegs() {
+ return getEquipmentItem(EquipmentSlot.LEGS);
+ }
+
+ /**
+ * Get the equipped boots.
+ */
+ public Item getBoots() {
+ return getEquipmentItem(EquipmentSlot.FEET);
+ }
+
+ /**
+ * Get the equipped gloves.
+ */
+ public Item getGloves() {
+ return getEquipmentItem(EquipmentSlot.HANDS);
+ }
+
+ /**
+ * Get the equipped cape.
+ */
+ public Item getCape() {
+ return getEquipmentItem(EquipmentSlot.CAPE);
+ }
+
+ /**
+ * Get the equipped amulet.
+ */
+ public Item getAmulet() {
+ return getEquipmentItem(EquipmentSlot.AMULET);
+ }
+
+ /**
+ * Get the equipped ring.
+ */
+ public Item getRing() {
+ return getEquipmentItem(EquipmentSlot.RING);
+ }
+
+ /**
+ * Get the equipped arrows.
+ */
+ public Item getArrows() {
+ return getEquipmentItem(EquipmentSlot.AMMO);
+ }
+
+ // === UTILITY METHODS ===
+
+ /**
+ * Get all non-empty items in the inventory.
+ */
+ public List
- getAllItems() {
+ return Arrays.stream(getInventory())
+ .filter(item -> !item.isEmpty())
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Get all equipped items.
+ */
+ public List
- getAllEquippedItems() {
+ return Arrays.stream(getEquipment())
+ .filter(item -> !item.isEmpty())
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Check if the inventory contains food (items with "eat" action).
+ */
+ public boolean hasFood() {
+ return Arrays.stream(getInventory())
+ .filter(item -> !item.isEmpty())
+ .anyMatch(item -> Arrays.asList(item.getActions()).contains("Eat"));
+ }
+
+ /**
+ * Get the first food item in the inventory.
+ */
+ public Item getFirstFood() {
+ return Arrays.stream(getInventory())
+ .filter(item -> !item.isEmpty())
+ .filter(item -> Arrays.asList(item.getActions()).contains("Eat"))
+ .findFirst()
+ .orElse(Item.EMPTY);
+ }
+}
+
+/**
+ * Equipment slot enumeration.
+ */
+enum EquipmentSlot {
+ HEAD(0),
+ CAPE(1),
+ AMULET(2),
+ WEAPON(3),
+ BODY(4),
+ SHIELD(5),
+ LEGS(7),
+ HANDS(9),
+ FEET(10),
+ RING(12),
+ AMMO(13);
+
+ private final int slotIndex;
+
+ EquipmentSlot(int slotIndex) {
+ this.slotIndex = slotIndex;
+ }
+
+ public int getSlotIndex() {
+ return slotIndex;
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/NavigationAPI.java b/modernized-client/src/main/java/com/openosrs/client/api/NavigationAPI.java
index 083beab..f6b1f76 100644
--- a/modernized-client/src/main/java/com/openosrs/client/api/NavigationAPI.java
+++ b/modernized-client/src/main/java/com/openosrs/client/api/NavigationAPI.java
@@ -2,6 +2,9 @@ package com.openosrs.client.api;
import com.openosrs.client.core.ClientCore;
import com.openosrs.client.engine.GameEngine;
+import com.openosrs.client.api.types.Path;
+import com.openosrs.client.api.types.Position;
+import com.openosrs.client.api.types.GameObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -13,10 +16,321 @@ import java.util.Set;
/**
* NavigationAPI - Provides pathfinding and navigation utilities.
- *
+ *
* This API module handles:
* - Path calculation and pathfinding
* - Distance calculations
* - Walkability checks
* - Navigation utilities
- */\npublic class NavigationAPI {\n private static final Logger logger = LoggerFactory.getLogger(NavigationAPI.class);\n \n private final ClientCore clientCore;\n private final GameEngine gameEngine;\n \n public NavigationAPI(ClientCore clientCore, GameEngine gameEngine) {\n this.clientCore = clientCore;\n this.gameEngine = gameEngine;\n }\n \n /**\n * Calculate a path to a destination using A* pathfinding.\n */\n public Path calculatePath(Position destination) {\n Position start = clientCore.getPlayerState().getPosition();\n if (start == null || destination == null) {\n logger.warn(\"Cannot calculate path - invalid start or destination\");\n return new Path(new Position[0]);\n }\n \n if (start.equals(destination)) {\n logger.debug(\"Already at destination\");\n return new Path(new Position[]{start});\n }\n \n logger.debug(\"Calculating path from {} to {}\", start, destination);\n \n try {\n List
pathList = aStar(start, destination);\n return new Path(pathList.toArray(new Position[0]));\n } catch (Exception e) {\n logger.error(\"Error calculating path\", e);\n return new Path(new Position[0]);\n }\n }\n \n /**\n * Check if a position is reachable.\n */\n public boolean isReachable(Position position) {\n if (position == null) {\n return false;\n }\n \n Position playerPos = clientCore.getPlayerState().getPosition();\n if (playerPos == null) {\n return false;\n }\n \n // If it's the same position, it's reachable\n if (playerPos.equals(position)) {\n return true;\n }\n \n // Check if the destination is walkable\n if (!isWalkable(position)) {\n return false;\n }\n \n // For now, assume positions within reasonable distance are reachable\n // TODO: Implement proper reachability analysis\n double distance = getDistanceTo(position);\n return distance <= 100; // Arbitrary limit for now\n }\n \n /**\n * Get the distance to a position.\n */\n public double getDistanceTo(Position position) {\n Position playerPos = clientCore.getPlayerState().getPosition();\n if (playerPos == null || position == null) {\n return Double.MAX_VALUE;\n }\n \n return playerPos.distanceTo(position);\n }\n \n /**\n * Check if a tile is walkable.\n */\n public boolean isWalkable(Position position) {\n if (position == null) {\n return false;\n }\n \n // TODO: Implement proper walkability checking\n // This would need to check:\n // - Terrain collision data\n // - Object blocking\n // - NPC blocking\n // - Player blocking\n // - Water/lava/etc.\n \n // For now, basic checks\n if (position.getX() < 0 || position.getY() < 0) {\n return false;\n }\n \n // Check if there are blocking objects at this position\n List objectsAtPos = clientCore.getWorldState().getGameObjects().stream()\n .filter(obj -> obj.getPosition().equals(position))\n .toList();\n \n for (GameObject obj : objectsAtPos) {\n // TODO: Check if object is blocking\n // For now, assume some object types are blocking\n if (obj.getName() != null && \n (obj.getName().toLowerCase().contains(\"wall\") ||\n obj.getName().toLowerCase().contains(\"door\") ||\n obj.getName().toLowerCase().contains(\"gate\"))) {\n return false;\n }\n }\n \n return true;\n }\n \n /**\n * Get the closest walkable position to a target.\n */\n public Position getClosestWalkablePosition(Position target) {\n if (target == null) {\n return null;\n }\n \n if (isWalkable(target)) {\n return target;\n }\n \n // Search in expanding circles for a walkable position\n for (int radius = 1; radius <= 5; radius++) {\n for (int dx = -radius; dx <= radius; dx++) {\n for (int dy = -radius; dy <= radius; dy++) {\n if (Math.abs(dx) == radius || Math.abs(dy) == radius) {\n Position candidate = target.offset(dx, dy);\n if (isWalkable(candidate)) {\n return candidate;\n }\n }\n }\n }\n }\n \n logger.warn(\"No walkable position found near {}\", target);\n return null;\n }\n \n /**\n * Get all adjacent walkable positions.\n */\n public List getAdjacentWalkablePositions(Position center) {\n List adjacent = new ArrayList<>();\n \n if (center == null) {\n return adjacent;\n }\n \n int[] dx = {-1, -1, -1, 0, 0, 1, 1, 1};\n int[] dy = {-1, 0, 1, -1, 1, -1, 0, 1};\n \n for (int i = 0; i < dx.length; i++) {\n Position pos = center.offset(dx[i], dy[i]);\n if (isWalkable(pos)) {\n adjacent.add(pos);\n }\n }\n \n return adjacent;\n }\n \n /**\n * Simple A* pathfinding implementation.\n */\n private List aStar(Position start, Position goal) {\n PriorityQueue openSet = new PriorityQueue<>();\n Set closedSet = new HashSet<>();\n \n Node startNode = new Node(start, null, 0, heuristic(start, goal));\n openSet.offer(startNode);\n \n int maxIterations = 1000; // Prevent infinite loops\n int iterations = 0;\n \n while (!openSet.isEmpty() && iterations < maxIterations) {\n iterations++;\n \n Node current = openSet.poll();\n \n if (current.position.equals(goal)) {\n // Reconstruct path\n List path = new ArrayList<>();\n Node node = current;\n while (node != null) {\n path.add(0, node.position);\n node = node.parent;\n }\n logger.debug(\"Path found with {} steps in {} iterations\", path.size(), iterations);\n return path;\n }\n \n closedSet.add(current.position);\n \n // Check all adjacent positions\n for (Position adjacent : getAdjacentWalkablePositions(current.position)) {\n if (closedSet.contains(adjacent)) {\n continue;\n }\n \n double gScore = current.gScore + current.position.distanceTo(adjacent);\n double fScore = gScore + heuristic(adjacent, goal);\n \n // Check if this path to adjacent is better\n boolean inOpenSet = openSet.stream().anyMatch(n -> n.position.equals(adjacent));\n if (!inOpenSet) {\n Node adjacentNode = new Node(adjacent, current, gScore, fScore);\n openSet.offer(adjacentNode);\n }\n }\n }\n \n logger.warn(\"No path found from {} to {} after {} iterations\", start, goal, iterations);\n return new ArrayList<>();\n }\n \n /**\n * Heuristic function for A* (Manhattan distance).\n */\n private double heuristic(Position a, Position b) {\n return Math.abs(a.getX() - b.getX()) + Math.abs(a.getY() - b.getY());\n }\n \n /**\n * Check if two positions are adjacent (within 1 tile).\n */\n public boolean isAdjacent(Position pos1, Position pos2) {\n if (pos1 == null || pos2 == null) {\n return false;\n }\n \n if (pos1.getPlane() != pos2.getPlane()) {\n return false;\n }\n \n int dx = Math.abs(pos1.getX() - pos2.getX());\n int dy = Math.abs(pos1.getY() - pos2.getY());\n \n return dx <= 1 && dy <= 1 && (dx + dy) > 0;\n }\n \n /**\n * Check if a position is within interaction range.\n */\n public boolean isInInteractionRange(Position target) {\n Position playerPos = clientCore.getPlayerState().getPosition();\n if (playerPos == null || target == null) {\n return false;\n }\n \n double distance = playerPos.distanceTo(target);\n return distance <= 1.5; // Standard interaction range\n }\n \n /**\n * Get a random walkable position within a radius.\n */\n public Position getRandomWalkablePosition(Position center, int radius) {\n if (center == null || radius <= 0) {\n return null;\n }\n \n List candidates = new ArrayList<>();\n \n for (int dx = -radius; dx <= radius; dx++) {\n for (int dy = -radius; dy <= radius; dy++) {\n Position candidate = center.offset(dx, dy);\n if (isWalkable(candidate) && center.distanceTo(candidate) <= radius) {\n candidates.add(candidate);\n }\n }\n }\n \n if (candidates.isEmpty()) {\n return null;\n }\n \n return candidates.get((int) (Math.random() * candidates.size()));\n }\n \n /**\n * Node class for A* pathfinding.\n */\n private static class Node implements Comparable {\n final Position position;\n final Node parent;\n final double gScore; // Cost from start\n final double fScore; // gScore + heuristic\n \n Node(Position position, Node parent, double gScore, double fScore) {\n this.position = position;\n this.parent = parent;\n this.gScore = gScore;\n this.fScore = fScore;\n }\n \n @Override\n public int compareTo(Node other) {\n return Double.compare(this.fScore, other.fScore);\n }\n }\n}"
\ No newline at end of file
+ */
+public class NavigationAPI {
+ private static final Logger logger = LoggerFactory.getLogger(NavigationAPI.class);
+
+ private final ClientCore clientCore;
+ private final GameEngine gameEngine;
+
+ public NavigationAPI(ClientCore clientCore, GameEngine gameEngine) {
+ this.clientCore = clientCore;
+ this.gameEngine = gameEngine;
+ }
+
+ /**
+ * Calculate a path to a destination using A* pathfinding.
+ */
+ public Path calculatePath(Position destination) {
+ Position start = clientCore.getPlayerState().getPosition();
+ if (start == null || destination == null) {
+ logger.warn("Cannot calculate path - invalid start or destination");
+ return new Path(new Position[0]);
+ }
+
+ if (start.equals(destination)) {
+ logger.debug("Already at destination");
+ return new Path(new Position[]{start});
+ }
+
+ logger.debug("Calculating path from {} to {}", start, destination);
+
+ try {
+ List pathList = aStar(start, destination);
+ return new Path(pathList.toArray(new Position[0]));
+ } catch (Exception e) {
+ logger.error("Error calculating path", e);
+ return new Path(new Position[0]);
+ }
+ }
+
+ /**
+ * Check if a position is reachable.
+ */
+ public boolean isReachable(Position position) {
+ if (position == null) {
+ return false;
+ }
+
+ Position playerPos = clientCore.getPlayerState().getPosition();
+ if (playerPos == null) {
+ return false;
+ }
+
+ // If it's the same position, it's reachable
+ if (playerPos.equals(position)) {
+ return true;
+ }
+
+ // Check if the destination is walkable
+ if (!isWalkable(position)) {
+ return false;
+ }
+
+ // For now, assume positions within reasonable distance are reachable
+ // TODO: Implement proper reachability analysis
+ double distance = getDistanceTo(position);
+ return distance <= 100; // Arbitrary limit for now
+ }
+
+ /**
+ * Get the distance to a position.
+ */
+ public double getDistanceTo(Position position) {
+ Position playerPos = clientCore.getPlayerState().getPosition();
+ if (playerPos == null || position == null) {
+ return Double.MAX_VALUE;
+ }
+
+ return playerPos.distanceTo(position);
+ }
+
+ /**
+ * Check if a tile is walkable.
+ */
+ public boolean isWalkable(Position position) {
+ if (position == null) {
+ return false;
+ }
+
+ // TODO: Implement proper walkability checking
+ // This would need to check:
+ // - Terrain collision data
+ // - Object blocking
+ // - NPC blocking
+ // - Player blocking
+ // - Water/lava/etc.
+
+ // For now, basic checks
+ if (position.getX() < 0 || position.getY() < 0) {
+ return false;
+ }
+
+ // Check if there are blocking objects at this position
+ List objectsAtPos = clientCore.getWorldState().getGameObjects().stream()
+ .filter(obj -> obj.getPosition().equals(position))
+ .toList();
+
+ for (GameObject obj : objectsAtPos) {
+ // TODO: Check if object is blocking
+ // For now, assume some object types are blocking
+ if (obj.getName() != null &&
+ (obj.getName().toLowerCase().contains("wall") ||
+ obj.getName().toLowerCase().contains("door") ||
+ obj.getName().toLowerCase().contains("gate"))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the closest walkable position to a target.
+ */
+ public Position getClosestWalkablePosition(Position target) {
+ if (target == null) {
+ return null;
+ }
+
+ if (isWalkable(target)) {
+ return target;
+ }
+
+ // Search in expanding circles for a walkable position
+ for (int radius = 1; radius <= 5; radius++) {
+ for (int dx = -radius; dx <= radius; dx++) {
+ for (int dy = -radius; dy <= radius; dy++) {
+ if (Math.abs(dx) == radius || Math.abs(dy) == radius) {
+ Position candidate = target.offset(dx, dy);
+ if (isWalkable(candidate)) {
+ return candidate;
+ }
+ }
+ }
+ }
+ }
+
+ logger.warn("No walkable position found near {}", target);
+ return null;
+ }
+
+ /**
+ * Get all adjacent walkable positions.
+ */
+ public List getAdjacentWalkablePositions(Position center) {
+ List adjacent = new ArrayList<>();
+
+ if (center == null) {
+ return adjacent;
+ }
+
+ int[] dx = {-1, -1, -1, 0, 0, 1, 1, 1};
+ int[] dy = {-1, 0, 1, -1, 1, -1, 0, 1};
+
+ for (int i = 0; i < dx.length; i++) {
+ Position pos = center.offset(dx[i], dy[i]);
+ if (isWalkable(pos)) {
+ adjacent.add(pos);
+ }
+ }
+
+ return adjacent;
+ }
+
+ /**
+ * Simple A* pathfinding implementation.
+ */
+ private List aStar(Position start, Position goal) {
+ PriorityQueue openSet = new PriorityQueue<>();
+ Set closedSet = new HashSet<>();
+
+ Node startNode = new Node(start, null, 0, heuristic(start, goal));
+ openSet.offer(startNode);
+
+ int maxIterations = 1000; // Prevent infinite loops
+ int iterations = 0;
+
+ while (!openSet.isEmpty() && iterations < maxIterations) {
+ iterations++;
+
+ Node current = openSet.poll();
+
+ if (current.position.equals(goal)) {
+ // Reconstruct path
+ List path = new ArrayList<>();
+ Node node = current;
+ while (node != null) {
+ path.add(0, node.position);
+ node = node.parent;
+ }
+ logger.debug("Path found with {} steps in {} iterations", path.size(), iterations);
+ return path;
+ }
+
+ closedSet.add(current.position);
+
+ // Check all adjacent positions
+ for (Position adjacent : getAdjacentWalkablePositions(current.position)) {
+ if (closedSet.contains(adjacent)) {
+ continue;
+ }
+
+ double gScore = current.gScore + current.position.distanceTo(adjacent);
+ double fScore = gScore + heuristic(adjacent, goal);
+
+ // Check if this path to adjacent is better
+ boolean inOpenSet = openSet.stream().anyMatch(n -> n.position.equals(adjacent));
+ if (!inOpenSet) {
+ Node adjacentNode = new Node(adjacent, current, gScore, fScore);
+ openSet.offer(adjacentNode);
+ }
+ }
+ }
+
+ logger.warn("No path found from {} to {} after {} iterations", start, goal, iterations);
+ return new ArrayList<>();
+ }
+
+ /**
+ * Heuristic function for A* (Manhattan distance).
+ */
+ private double heuristic(Position a, Position b) {
+ return Math.abs(a.getX() - b.getX()) + Math.abs(a.getY() - b.getY());
+ }
+
+ /**
+ * Check if two positions are adjacent (within 1 tile).
+ */
+ public boolean isAdjacent(Position pos1, Position pos2) {
+ if (pos1 == null || pos2 == null) {
+ return false;
+ }
+
+ if (pos1.getPlane() != pos2.getPlane()) {
+ return false;
+ }
+
+ int dx = Math.abs(pos1.getX() - pos2.getX());
+ int dy = Math.abs(pos1.getY() - pos2.getY());
+
+ return dx <= 1 && dy <= 1 && (dx + dy) > 0;
+ }
+
+ /**
+ * Check if a position is within interaction range.
+ */
+ public boolean isInInteractionRange(Position target) {
+ Position playerPos = clientCore.getPlayerState().getPosition();
+ if (playerPos == null || target == null) {
+ return false;
+ }
+
+ double distance = playerPos.distanceTo(target);
+ return distance <= 1.5; // Standard interaction range
+ }
+
+ /**
+ * Get a random walkable position within a radius.
+ */
+ public Position getRandomWalkablePosition(Position center, int radius) {
+ if (center == null || radius <= 0) {
+ return null;
+ }
+
+ List candidates = new ArrayList<>();
+
+ for (int dx = -radius; dx <= radius; dx++) {
+ for (int dy = -radius; dy <= radius; dy++) {
+ Position candidate = center.offset(dx, dy);
+ if (isWalkable(candidate) && center.distanceTo(candidate) <= radius) {
+ candidates.add(candidate);
+ }
+ }
+ }
+
+ if (candidates.isEmpty()) {
+ return null;
+ }
+
+ return candidates.get((int) (Math.random() * candidates.size()));
+ }
+
+ /**
+ * Node class for A* pathfinding.
+ */
+ private static class Node implements Comparable {
+ final Position position;
+ final Node parent;
+ final double gScore; // Cost from start
+ final double fScore; // gScore + heuristic
+
+ Node(Position position, Node parent, double gScore, double fScore) {
+ this.position = position;
+ this.parent = parent;
+ this.gScore = gScore;
+ this.fScore = fScore;
+ }
+
+ @Override
+ public int compareTo(Node other) {
+ return Double.compare(this.fScore, other.fScore);
+ }
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/PlayerAPI.java b/modernized-client/src/main/java/com/openosrs/client/api/PlayerAPI.java
index a3ac608..94d1a33 100644
--- a/modernized-client/src/main/java/com/openosrs/client/api/PlayerAPI.java
+++ b/modernized-client/src/main/java/com/openosrs/client/api/PlayerAPI.java
@@ -1,14 +1,16 @@
package com.openosrs.client.api;
import com.openosrs.client.core.ClientCore;
-import com.openosrs.client.core.state.PlayerState;
+import com.openosrs.client.core.PlayerState;
+import com.openosrs.client.api.types.Position;
+import com.openosrs.client.api.types.Skill;
import com.openosrs.client.core.bridge.BridgeAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* PlayerAPI - Provides access to player-specific information and state.
- *
+ *
* This API module handles:
* - Player position and movement
* - Player stats (hitpoints, prayer, run energy)
@@ -17,150 +19,150 @@ import org.slf4j.LoggerFactory;
*/
public class PlayerAPI {
private static final Logger logger = LoggerFactory.getLogger(PlayerAPI.class);
-
+
private final ClientCore clientCore;
private final PlayerState playerState;
private final BridgeAdapter bridgeAdapter;
-
+
public PlayerAPI(ClientCore clientCore, BridgeAdapter bridgeAdapter) {
this.clientCore = clientCore;
this.playerState = clientCore.getPlayerState();
this.bridgeAdapter = bridgeAdapter;
}
-
+
/**
* Get the player's current position.
*/
public Position getPosition() {
return bridgeAdapter.getPlayerPosition();
}
-
+
/**
* Get the player's current hitpoints.
*/
public int getHitpoints() {
return bridgeAdapter.getSkillLevel(Skill.HITPOINTS);
}
-
+
/**
* Get the player's maximum hitpoints.
*/
public int getMaxHitpoints() {
return bridgeAdapter.getSkillRealLevel(Skill.HITPOINTS);
}
-
+
/**
* Get the player's current prayer points.
*/
public int getPrayer() {
return bridgeAdapter.getSkillLevel(Skill.PRAYER);
}
-
+
/**
* Get the player's maximum prayer points.
*/
public int getMaxPrayer() {
return bridgeAdapter.getSkillRealLevel(Skill.PRAYER);
}
-
+
/**
* Get the player's current run energy (0-100).
*/
public int getRunEnergy() {
return bridgeAdapter.getRunEnergy();
}
-
+
/**
* Get the player's combat level.
*/
public int getCombatLevel() {
return playerState.getCombatLevel();
}
-
+
/**
* Get a specific skill level (current level including temporary boosts/drains).
*/
public int getSkillLevel(Skill skill) {
return bridgeAdapter.getSkillLevel(skill);
}
-
+
/**
* Get boosted skill level (same as getSkillLevel, for clarity).
*/
public int getBoostedSkillLevel(Skill skill) {
return bridgeAdapter.getSkillLevel(skill);
}
-
+
/**
* Get the real/base skill level (without temporary boosts/drains).
*/
public int getRealSkillLevel(Skill skill) {
return bridgeAdapter.getSkillRealLevel(skill);
}
-
+
/**
* Get skill experience.
*/
public int getSkillExperience(Skill skill) {
return bridgeAdapter.getSkillExperience(skill);
}
-
+
/**
* Check if the player is currently in combat.
*/
public boolean isInCombat() {
return playerState.isInCombat();
}
-
+
/**
* Get the player's current animation ID (-1 if no animation).
*/
public int getCurrentAnimation() {
return bridgeAdapter.getCurrentAnimation();
}
-
+
/**
* Get the player's username.
*/
public String getUsername() {
return bridgeAdapter.getUsername();
}
-
+
/**
* Check if the player is moving.
*/
public boolean isMoving() {
return bridgeAdapter.isMoving();
}
-
+
/**
* Check if the player is idle (not animating, not moving).
*/
public boolean isIdle() {
return !isMoving() && getCurrentAnimation() == -1;
}
-
+
/**
* Get the player's current facing direction (0-7).
*/
public int getFacingDirection() {
return playerState.getFacingDirection();
}
-
+
/**
* Check if the player has run mode enabled.
*/
public boolean isRunModeEnabled() {
return playerState.isRunModeEnabled();
}
-
+
/**
* Get the player's current overhead text (if any).
*/
public String getOverheadText() {
return playerState.getOverheadText();
}
-
+
/**
* Get the player's total level (sum of all skills).
*/
@@ -171,7 +173,7 @@ public class PlayerAPI {
}
return total;
}
-
+
/**
* Check if the player is at a specific position.
*/
@@ -179,7 +181,7 @@ public class PlayerAPI {
Position currentPos = getPosition();
return currentPos != null && currentPos.equals(position);
}
-
+
/**
* Get the distance to a specific position.
*/
@@ -187,11 +189,11 @@ public class PlayerAPI {
Position currentPos = getPosition();
return currentPos != null ? currentPos.distanceTo(position) : Double.MAX_VALUE;
}
-
+
/**
* Check if the player is within a certain distance of a position.
*/
public boolean isWithinDistance(Position position, double maxDistance) {
return getDistanceTo(position) <= maxDistance;
}
-}
\ No newline at end of file
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/WorldAPI.java b/modernized-client/src/main/java/com/openosrs/client/api/WorldAPI.java
index 59eb299..f10ade3 100644
--- a/modernized-client/src/main/java/com/openosrs/client/api/WorldAPI.java
+++ b/modernized-client/src/main/java/com/openosrs/client/api/WorldAPI.java
@@ -2,6 +2,10 @@ package com.openosrs.client.api;
import com.openosrs.client.core.ClientCore;
import com.openosrs.client.core.bridge.BridgeAdapter;
+import com.openosrs.client.api.types.NPC;
+import com.openosrs.client.api.types.GameObject;
+import com.openosrs.client.api.types.GroundItem;
+import com.openosrs.client.api.types.Position;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -10,7 +14,7 @@ import java.util.stream.Collectors;
/**
* WorldAPI - Provides access to world objects, NPCs, players, and ground items.
- *
+ *
* This API module handles:
* - NPCs (Non-Player Characters)
* - Game Objects (doors, trees, rocks, etc.)
@@ -20,24 +24,24 @@ import java.util.stream.Collectors;
*/
public class WorldAPI {
private static final Logger logger = LoggerFactory.getLogger(WorldAPI.class);
-
+
private final ClientCore clientCore;
private final BridgeAdapter bridgeAdapter;
-
+
public WorldAPI(ClientCore clientCore, BridgeAdapter bridgeAdapter) {
this.clientCore = clientCore;
this.bridgeAdapter = bridgeAdapter;
}
-
+
// === NPC METHODS ===
-
+
/**
* Get all NPCs currently visible.
*/
public List getNPCs() {
return bridgeAdapter.getNPCs();
}
-
+
/**
* Get NPCs by name.
*/
@@ -46,7 +50,7 @@ public class WorldAPI {
.filter(npc -> npc.getName().equalsIgnoreCase(name))
.collect(Collectors.toList());
}
-
+
/**
* Get NPCs by ID.
*/
@@ -55,16 +59,16 @@ public class WorldAPI {
.filter(npc -> npc.getId() == id)
.collect(Collectors.toList());
}
-
+
/**
* Get the closest NPC to the player.
*/
public NPC getClosestNPC() {
- Position playerPos = bridgeAdapter.getPlayerPosition();
+ Position playerPos = bridgeAdapter.getPlayerPosition(); // TODO: Cannot convert type Object to type Position
if (playerPos == null) {
return null;
}
-
+
return getNPCs().stream()
.min((a, b) -> Double.compare(
playerPos.distanceTo(a.getPosition()),
@@ -72,16 +76,16 @@ public class WorldAPI {
))
.orElse(null);
}
-
+
/**
* Get the closest NPC with the specified name.
*/
public NPC getClosestNPC(String name) {
- Position playerPos = bridgeAdapter.getPlayerPosition();
+ Position playerPos = bridgeAdapter.getPlayerPosition(); // TODO: Cannot convert type Object to type Position
if (playerPos == null) {
return null;
}
-
+
return getNPCs(name).stream()
.min((a, b) -> Double.compare(
playerPos.distanceTo(a.getPosition()),
@@ -89,16 +93,16 @@ public class WorldAPI {
))
.orElse(null);
}
-
+
/**
* Get the closest NPC with the specified ID.
*/
public NPC getClosestNPC(int id) {
- Position playerPos = bridgeAdapter.getPlayerPosition();
+ Position playerPos = bridgeAdapter.getPlayerPosition(); // TODO: Cannot convert type Object to type Position
if (playerPos == null) {
return null;
}
-
+
return getNPCs(id).stream()
.min((a, b) -> Double.compare(
playerPos.distanceTo(a.getPosition()),
@@ -106,21 +110,21 @@ public class WorldAPI {
))
.orElse(null);
}
-
+
/**
* Get NPCs within a certain distance of the player.
*/
public List getNPCsWithinDistance(double maxDistance) {
- Position playerPos = bridgeAdapter.getPlayerPosition();
+ Position playerPos = bridgeAdapter.getPlayerPosition(); // TODO: Cannot convert type Object to type Position
if (playerPos == null) {
return List.of();
}
-
+
return getNPCs().stream()
.filter(npc -> playerPos.distanceTo(npc.getPosition()) <= maxDistance)
.collect(Collectors.toList());
}
-
+
/**
* Get NPCs within a certain distance of a position.
*/
@@ -129,16 +133,16 @@ public class WorldAPI {
.filter(npc -> position.distanceTo(npc.getPosition()) <= maxDistance)
.collect(Collectors.toList());
}
-
+
// === GAME OBJECT METHODS ===
-
+
/**
* Get all game objects currently visible.
*/
- public List getGameObjects() {
+ public List getGameObjects() { // TODO: Type Mismatch between List and List
return bridgeAdapter.getGameObjects();
}
-
+
/**
* Get game objects by name.
*/
@@ -147,7 +151,7 @@ public class WorldAPI {
.filter(obj -> obj.getName().equalsIgnoreCase(name))
.collect(Collectors.toList());
}
-
+
/**
* Get game objects by ID.
*/
@@ -156,16 +160,16 @@ public class WorldAPI {
.filter(obj -> obj.getId() == id)
.collect(Collectors.toList());
}
-
+
/**
* Get the closest game object to the player.
*/
public GameObject getClosestGameObject() {
- Position playerPos = bridgeAdapter.getPlayerPosition();
+ Position playerPos = bridgeAdapter.getPlayerPosition(); // TODO: Cannot convert type Object to type Position
if (playerPos == null) {
return null;
}
-
+
return getGameObjects().stream()
.min((a, b) -> Double.compare(
playerPos.distanceTo(a.getPosition()),
@@ -173,16 +177,16 @@ public class WorldAPI {
))
.orElse(null);
}
-
+
/**
* Get the closest game object with the specified name.
*/
public GameObject getClosestGameObject(String name) {
- Position playerPos = bridgeAdapter.getPlayerPosition();
+ Position playerPos = bridgeAdapter.getPlayerPosition(); // TODO: Cannot convert type Object to type Position
if (playerPos == null) {
return null;
}
-
+
return getGameObjects(name).stream()
.min((a, b) -> Double.compare(
playerPos.distanceTo(a.getPosition()),
@@ -190,16 +194,16 @@ public class WorldAPI {
))
.orElse(null);
}
-
+
/**
* Get the closest game object with the specified ID.
*/
public GameObject getClosestGameObject(int id) {
- Position playerPos = bridgeAdapter.getPlayerPosition();
+ Position playerPos = bridgeAdapter.getPlayerPosition(); // TODO: Cannot convert type Object to type Position
if (playerPos == null) {
return null;
}
-
+
return getGameObjects(id).stream()
.min((a, b) -> Double.compare(
playerPos.distanceTo(a.getPosition()),
@@ -207,21 +211,21 @@ public class WorldAPI {
))
.orElse(null);
}
-
+
/**
* Get game objects within a certain distance of the player.
*/
public List getGameObjectsWithinDistance(double maxDistance) {
- Position playerPos = bridgeAdapter.getPlayerPosition();
+ Position playerPos = bridgeAdapter.getPlayerPosition(); // TODO: Cannot convert type Object to type Position
if (playerPos == null) {
return List.of();
}
-
+
return getGameObjects().stream()
.filter(obj -> playerPos.distanceTo(obj.getPosition()) <= maxDistance)
.collect(Collectors.toList());
}
-
+
/**
* Get game objects within a certain distance of a position.
*/
@@ -230,16 +234,16 @@ public class WorldAPI {
.filter(obj -> position.distanceTo(obj.getPosition()) <= maxDistance)
.collect(Collectors.toList());
}
-
+
// === GROUND ITEM METHODS ===
-
+
/**
* Get all ground items currently visible.
*/
public List getGroundItems() {
- return bridgeAdapter.getGroundItems();
+ return bridgeAdapter.getGroundItems(); // TODO: Type Mismatch between List and List
}
-
+
/**
* Get ground items by name.
*/
@@ -248,25 +252,25 @@ public class WorldAPI {
.filter(item -> item.getName().equalsIgnoreCase(name))
.collect(Collectors.toList());
}
-
+
/**
* Get ground items by ID.
*/
public List getGroundItems(int id) {
return getGroundItems().stream()
- .filter(item -> item.getId() == id)
+ .filter(item -> item.getId() == id) // TODO: Fix getId() method in GroundItem
.collect(Collectors.toList());
}
-
+
/**
* Get the closest ground item to the player.
*/
public GroundItem getClosestGroundItem() {
- Position playerPos = bridgeAdapter.getPlayerPosition();
+ Position playerPos = bridgeAdapter.getPlayerPosition(); // TODO: Cannot convert type Object to type Position
if (playerPos == null) {
return null;
}
-
+
return getGroundItems().stream()
.min((a, b) -> Double.compare(
playerPos.distanceTo(a.getPosition()),
@@ -274,16 +278,16 @@ public class WorldAPI {
))
.orElse(null);
}
-
+
/**
* Get the closest ground item with the specified name.
*/
public GroundItem getClosestGroundItem(String name) {
- Position playerPos = bridgeAdapter.getPlayerPosition();
+ Position playerPos = bridgeAdapter.getPlayerPosition(); // TODO: Cannot convert type Object to type Position
if (playerPos == null) {
return null;
}
-
+
return getGroundItems(name).stream()
.min((a, b) -> Double.compare(
playerPos.distanceTo(a.getPosition()),
@@ -291,16 +295,16 @@ public class WorldAPI {
))
.orElse(null);
}
-
+
/**
* Get the closest ground item with the specified ID.
*/
public GroundItem getClosestGroundItem(int id) {
- Position playerPos = bridgeAdapter.getPlayerPosition();
+ Position playerPos = bridgeAdapter.getPlayerPosition(); // TODO: Cannot convert type Object to type Position
if (playerPos == null) {
return null;
}
-
+
return getGroundItems(id).stream()
.min((a, b) -> Double.compare(
playerPos.distanceTo(a.getPosition()),
@@ -308,21 +312,21 @@ public class WorldAPI {
))
.orElse(null);
}
-
+
/**
* Get ground items within a certain distance of the player.
*/
public List getGroundItemsWithinDistance(double maxDistance) {
- Position playerPos = bridgeAdapter.getPlayerPosition();
+ Position playerPos = bridgeAdapter.getPlayerPosition(); // TODO: Cannot convert type Object to type Position
if (playerPos == null) {
return List.of();
}
-
+
return getGroundItems().stream()
.filter(item -> playerPos.distanceTo(item.getPosition()) <= maxDistance)
.collect(Collectors.toList());
}
-
+
/**
* Get ground items within a certain distance of a position.
*/
@@ -331,9 +335,9 @@ public class WorldAPI {
.filter(item -> position.distanceTo(item.getPosition()) <= maxDistance)
.collect(Collectors.toList());
}
-
+
// === UTILITY METHODS ===
-
+
/**
* Check if a position is walkable.
*/
@@ -342,7 +346,7 @@ public class WorldAPI {
// For now, return true as a placeholder
return true;
}
-
+
/**
* Get the current world number.
*/
@@ -351,7 +355,7 @@ public class WorldAPI {
// For now, return a placeholder
return 301;
}
-
+
/**
* Check if an area is loaded.
*/
@@ -360,4 +364,4 @@ public class WorldAPI {
// For now, assume it's loaded if the position is reasonable
return position != null && position.getX() > 0 && position.getY() > 0;
}
-}
\ No newline at end of file
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/ChatMessage.java b/modernized-client/src/main/java/com/openosrs/client/api/types/ChatMessage.java
new file mode 100644
index 0000000..0b87076
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/ChatMessage.java
@@ -0,0 +1,30 @@
+package com.openosrs.client.api.types;
+
+public class ChatMessage {
+ private final String username;
+ private final String message;
+ private final int type;
+ private final long timestamp;
+
+ public ChatMessage(String username, String message, int type) {
+ this.username = username;
+ this.message = message;
+ this.type = type;
+ this.timestamp = System.currentTimeMillis();
+ }
+
+ public String getUsername() { return username; }
+ public String getMessage() { return message; }
+ public int getType() { return type; }
+ public long getTimestamp() { return timestamp; }
+
+ public boolean isPublicChat() { return type == 0; }
+ public boolean isPrivateMessage() { return type == 3; }
+ public boolean isFriendChat() { return type == 9; }
+ public boolean isClanChat() { return type == 11; }
+
+ @Override
+ public String toString() {
+ return String.format("ChatMessage[%s]: %s", username, message);
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/CombatStateChange.java b/modernized-client/src/main/java/com/openosrs/client/api/types/CombatStateChange.java
new file mode 100644
index 0000000..970c1a7
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/CombatStateChange.java
@@ -0,0 +1,21 @@
+package com.openosrs.client.api.types;
+
+public class CombatStateChange {
+ private final boolean inCombat;
+ private final NPC target;
+
+ public CombatStateChange(boolean inCombat, NPC target) {
+ this.inCombat = inCombat;
+ this.target = target;
+ }
+
+ public boolean isInCombat() { return inCombat; }
+ public NPC getTarget() { return target; }
+
+ @Override
+ public String toString() {
+ return String.format("CombatStateChange: %s%s",
+ inCombat ? "Entered combat" : "Exited combat",
+ target != null ? " with " + target.getName() : "");
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/EquipmentSlot.java b/modernized-client/src/main/java/com/openosrs/client/api/types/EquipmentSlot.java
new file mode 100644
index 0000000..ecf81dd
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/EquipmentSlot.java
@@ -0,0 +1,79 @@
+package com.openosrs.client.api.types;
+
+/**
+ * Enumeration of equipment slots available in RuneScape.
+ * These correspond to the wearable item slots in the game.
+ */
+public enum EquipmentSlot {
+ HEAD(0, "Head"),
+ CAPE(1, "Cape"),
+ AMULET(2, "Amulet"),
+ WEAPON(3, "Weapon"),
+ BODY(4, "Body"),
+ SHIELD(5, "Shield"),
+ LEGS(7, "Legs"),
+ GLOVES(9, "Gloves"),
+ BOOTS(10, "Boots"),
+ RING(12, "Ring"),
+ AMMO(13, "Ammo");
+
+ private final int slotId;
+ private final String name;
+
+ EquipmentSlot(int slotId, String name) {
+ this.slotId = slotId;
+ this.name = name;
+ }
+
+ /**
+ * Get the slot ID (used internally by RuneScape).
+ */
+ public int getSlotId() {
+ return slotId;
+ }
+
+ /**
+ * Get the human-readable slot name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Get equipment slot by ID.
+ */
+ public static EquipmentSlot fromSlotId(int slotId) {
+ for (EquipmentSlot slot : values()) {
+ if (slot.slotId == slotId) {
+ return slot;
+ }
+ }
+ throw new IllegalArgumentException("Unknown equipment slot ID: " + slotId);
+ }
+
+ /**
+ * Check if this is a combat-related equipment slot.
+ */
+ public boolean isCombatSlot() {
+ return this == WEAPON || this == SHIELD || this == AMMO;
+ }
+
+ /**
+ * Check if this is an armor slot.
+ */
+ public boolean isArmorSlot() {
+ return this == HEAD || this == BODY || this == LEGS || this == GLOVES || this == BOOTS;
+ }
+
+ /**
+ * Check if this is an accessory slot.
+ */
+ public boolean isAccessorySlot() {
+ return this == CAPE || this == AMULET || this == RING;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
\ No newline at end of file
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/ExperienceGain.java b/modernized-client/src/main/java/com/openosrs/client/api/types/ExperienceGain.java
new file mode 100644
index 0000000..8f5897e
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/ExperienceGain.java
@@ -0,0 +1,26 @@
+package com.openosrs.client.api.types;
+
+public class ExperienceGain {
+ private final AgentAPI.Skill skill;
+ private final int oldExperience;
+ private final int newExperience;
+ private final int gain;
+
+ public ExperienceGain(AgentAPI.Skill skill, int oldExperience, int newExperience) {
+ this.skill = skill;
+ this.oldExperience = oldExperience;
+ this.newExperience = newExperience;
+ this.gain = newExperience - oldExperience;
+ }
+
+ public AgentAPI.Skill getSkill() { return skill; }
+ public int getOldExperience() { return oldExperience; }
+ public int getNewExperience() { return newExperience; }
+ public int getGain() { return gain; }
+
+ @Override
+ public String toString() {
+ return String.format("ExperienceGain: %s +%d xp (total: %d)",
+ skill.name(), gain, newExperience);
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/GameObject.java b/modernized-client/src/main/java/com/openosrs/client/api/types/GameObject.java
new file mode 100644
index 0000000..ca59de2
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/GameObject.java
@@ -0,0 +1,45 @@
+package com.openosrs.client.api.types;
+
+public class GameObject {
+ private final int id;
+ private final String name;
+ private final Position position;
+ private final int type;
+ private final int orientation;
+ private final String[] actions;
+
+ public GameObject(int id, String name, Position position, int type,
+ int orientation, String[] actions) {
+ this.id = id;
+ this.name = name;
+ this.position = position;
+ this.type = type;
+ this.orientation = orientation;
+ this.actions = actions != null ? actions : new String[0];
+ }
+
+ public int getId() { return id; }
+ public String getName() { return name; }
+ public Position getPosition() { return position; }
+ public int getType() { return type; }
+ public int getOrientation() { return orientation; }
+ public String[] getActions() { return actions.clone(); }
+
+ public boolean hasAction(String action) {
+ for (String a : actions) {
+ if (a != null && a.equalsIgnoreCase(action)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public double distanceToPlayer(Position playerPos) {
+ return position.distanceTo(playerPos);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("GameObject[%d] %s at %s", id, name, position);
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/GroundItem.java b/modernized-client/src/main/java/com/openosrs/client/api/types/GroundItem.java
new file mode 100644
index 0000000..beeee9e
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/GroundItem.java
@@ -0,0 +1,40 @@
+package com.openosrs.client.api.types;
+
+public class GroundItem {
+ private final int itemId;
+ private final String name;
+ private final int quantity;
+ private final Position position;
+ private final long spawnTime;
+ private final boolean tradeable;
+
+ public GroundItem(int itemId, String name, int quantity, Position position,
+ long spawnTime, boolean tradeable) {
+ this.itemId = itemId;
+ this.name = name;
+ this.quantity = quantity;
+ this.position = position;
+ this.spawnTime = spawnTime;
+ this.tradeable = tradeable;
+ }
+
+ public int getItemId() { return itemId; }
+ public String getName() { return name; }
+ public int getQuantity() { return quantity; }
+ public Position getPosition() { return position; }
+ public long getSpawnTime() { return spawnTime; }
+ public boolean isTradeable() { return tradeable; }
+
+ public long getAge() {
+ return System.currentTimeMillis() - spawnTime;
+ }
+
+ public double distanceToPlayer(Position playerPos) {
+ return position.distanceTo(playerPos);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("GroundItem[%d] %s x%d at %s", itemId, name, quantity, position);
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/HealthInfo.java b/modernized-client/src/main/java/com/openosrs/client/api/types/HealthInfo.java
new file mode 100644
index 0000000..7c2a917
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/HealthInfo.java
@@ -0,0 +1,25 @@
+package com.openosrs.client.api.types;
+
+public class HealthInfo {
+ private final int current;
+ private final int max;
+ private final double percentage;
+
+ public HealthInfo(int current, int max) {
+ this.current = current;
+ this.max = max;
+ this.percentage = max > 0 ? (double) current / max : 0.0;
+ }
+
+ public int getCurrent() { return current; }
+ public int getMax() { return max; }
+ public double getPercentage() { return percentage; }
+
+ public boolean isLow() { return percentage < 0.3; }
+ public boolean isCritical() { return percentage < 0.15; }
+
+ @Override
+ public String toString() {
+ return String.format("Health: %d/%d (%.1f%%)", current, max, percentage * 100);
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/InventoryChange.java b/modernized-client/src/main/java/com/openosrs/client/api/types/InventoryChange.java
new file mode 100644
index 0000000..f21f48d
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/InventoryChange.java
@@ -0,0 +1,35 @@
+package com.openosrs.client.api.types;
+
+public class InventoryChange {
+ private final int slot;
+ private final Item oldItem;
+ private final Item newItem;
+
+ public InventoryChange(int slot, Item oldItem, Item newItem) {
+ this.slot = slot;
+ this.oldItem = oldItem;
+ this.newItem = newItem;
+ }
+
+ public int getSlot() { return slot; }
+ public Item getOldItem() { return oldItem; }
+ public Item getNewItem() { return newItem; }
+
+ public boolean isItemAdded() {
+ return oldItem.isEmpty() && !newItem.isEmpty();
+ }
+
+ public boolean isItemRemoved() {
+ return !oldItem.isEmpty() && newItem.isEmpty();
+ }
+
+ public boolean isQuantityChanged() {
+ return oldItem.getItemId() == newItem.getItemId() &&
+ oldItem.getQuantity() != newItem.getQuantity();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("InventoryChange[%d]: %s -> %s", slot, oldItem, newItem);
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/Item.java b/modernized-client/src/main/java/com/openosrs/client/api/types/Item.java
new file mode 100644
index 0000000..804d8ec
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/Item.java
@@ -0,0 +1,47 @@
+package com.openosrs.client.api.types;
+
+public class Item {
+ private final int itemId;
+ private final String name;
+ private final int quantity;
+ private final boolean stackable;
+ private final boolean tradeable;
+ private final String[] actions;
+
+ public static final Item EMPTY = new Item(-1, "Empty", 0, false, false, new String[0]);
+
+ public Item(int itemId, String name, int quantity, boolean stackable,
+ boolean tradeable, String[] actions) {
+ this.itemId = itemId;
+ this.name = name;
+ this.quantity = quantity;
+ this.stackable = stackable;
+ this.tradeable = tradeable;
+ this.actions = actions != null ? actions : new String[0];
+ }
+
+ public int getItemId() { return itemId; }
+ public String getName() { return name; }
+ public int getQuantity() { return quantity; }
+ public boolean isStackable() { return stackable; }
+ public boolean isTradeable() { return tradeable; }
+ public String[] getActions() { return actions.clone(); }
+
+ public boolean isEmpty() {
+ return itemId == -1 || quantity == 0;
+ }
+
+ public boolean hasAction(String action) {
+ for (String a : actions) {
+ if (a != null && a.equalsIgnoreCase(action)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return isEmpty() ? "Empty" : String.format("Item[%d] %s x%d", itemId, name, quantity);
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/NPC.java b/modernized-client/src/main/java/com/openosrs/client/api/types/NPC.java
new file mode 100644
index 0000000..3f777ca
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/NPC.java
@@ -0,0 +1,54 @@
+package com.openosrs.client.api.types;
+
+public class NPC {
+ private final int index;
+ private final int id;
+ private final String name;
+ private final Position position;
+ private final int hitpoints;
+ private final int maxHitpoints;
+ private final int combatLevel;
+ private final int animationId;
+ private final boolean inCombat;
+ private final String overheadText;
+
+ public NPC(int index, int id, String name, Position position,
+ int hitpoints, int maxHitpoints, int combatLevel,
+ int animationId, boolean inCombat, String overheadText) {
+ this.index = index;
+ this.id = id;
+ this.name = name;
+ this.position = position;
+ this.hitpoints = hitpoints;
+ this.maxHitpoints = maxHitpoints;
+ this.combatLevel = combatLevel;
+ this.animationId = animationId;
+ this.inCombat = inCombat;
+ this.overheadText = overheadText;
+ }
+
+ public int getIndex() { return index; }
+ public int getId() { return id; }
+ public String getName() { return name; }
+ public Position getPosition() { return position; }
+ public int getHitpoints() { return hitpoints; }
+ public int getMaxHitpoints() { return maxHitpoints; }
+ public int getCombatLevel() { return combatLevel; }
+ public int getAnimationId() { return animationId; }
+ public boolean isInCombat() { return inCombat; }
+ public String getOverheadText() { return overheadText; }
+
+ public double distanceToPlayer(Position playerPos) {
+ return position.distanceTo(playerPos);
+ }
+
+ public boolean isInteractable() {
+ // NPCs with -1 ID are typically not interactable
+ return id != -1;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("NPC[%d] %s (%d) at %s", index, name, id, position);
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/OtherPlayer.java b/modernized-client/src/main/java/com/openosrs/client/api/types/OtherPlayer.java
new file mode 100644
index 0000000..06d2f96
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/OtherPlayer.java
@@ -0,0 +1,39 @@
+package com.openosrs.client.api.types;
+
+public class OtherPlayer {
+ private final int index;
+ private final String username;
+ private final Position position;
+ private final int combatLevel;
+ private final int animationId;
+ private final String overheadText;
+ private final boolean inCombat;
+
+ public OtherPlayer(int index, String username, Position position,
+ int combatLevel, int animationId, String overheadText, boolean inCombat) {
+ this.index = index;
+ this.username = username;
+ this.position = position;
+ this.combatLevel = combatLevel;
+ this.animationId = animationId;
+ this.overheadText = overheadText;
+ this.inCombat = inCombat;
+ }
+
+ public int getIndex() { return index; }
+ public String getUsername() { return username; }
+ public Position getPosition() { return position; }
+ public int getCombatLevel() { return combatLevel; }
+ public int getAnimationId() { return animationId; }
+ public String getOverheadText() { return overheadText; }
+ public boolean isInCombat() { return inCombat; }
+
+ public double distanceToPlayer(Position playerPos) {
+ return position.distanceTo(playerPos);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Player[%d] %s (cb:%d) at %s", index, username, combatLevel, position);
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/Path.java b/modernized-client/src/main/java/com/openosrs/client/api/types/Path.java
new file mode 100644
index 0000000..7eea0a2
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/Path.java
@@ -0,0 +1,35 @@
+package com.openosrs.client.api.types;
+
+public class Path {
+ private final Position[] steps;
+ private final int length;
+
+ public Path(Position[] steps) {
+ this.steps = steps != null ? steps : new Position[0];
+ this.length = this.steps.length;
+ }
+
+ public Position[] getSteps() { return steps.clone(); }
+ public int getLength() { return length; }
+ public boolean isEmpty() { return length == 0; }
+
+ public Position getStep(int index) {
+ if (index >= 0 && index < length) {
+ return steps[index];
+ }
+ return null;
+ }
+
+ public Position getFirstStep() {
+ return length > 0 ? steps[0] : null;
+ }
+
+ public Position getLastStep() {
+ return length > 0 ? steps[length - 1] : null;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Path[%d steps]", length);
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/Position.java b/modernized-client/src/main/java/com/openosrs/client/api/types/Position.java
new file mode 100644
index 0000000..ad811d7
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/Position.java
@@ -0,0 +1,48 @@
+package com.openosrs.client.api.types;
+
+public class Position {
+ private final int x;
+ private final int y;
+ private final int plane;
+
+ public Position(int x, int y, int plane) {
+ this.x = x;
+ this.y = y;
+ this.plane = plane;
+ }
+
+ public int getX() { return x; }
+ public int getY() { return y; }
+ public int getPlane() { return plane; }
+
+ public double distanceTo(Position other) {
+ if (other.plane != this.plane) {
+ return Double.MAX_VALUE; // Different planes
+ }
+ int dx = other.x - this.x;
+ int dy = other.y - this.y;
+ return Math.sqrt(dx * dx + dy * dy);
+ }
+
+ public Position offset(int dx, int dy) {
+ return new Position(x + dx, y + dy, plane);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof Position)) return false;
+ Position other = (Position) obj;
+ return x == other.x && y == other.y && plane == other.plane;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * (31 * x + y) + plane;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Position(%d, %d, %d)", x, y, plane);
+ }
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/api/types/Skill.java b/modernized-client/src/main/java/com/openosrs/client/api/types/Skill.java
new file mode 100644
index 0000000..f73b6bb
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/api/types/Skill.java
@@ -0,0 +1,101 @@
+package com.openosrs.client.api.types;
+
+/**
+ * Enumeration of all RuneScape skills that agents can monitor and interact with.
+ * These correspond directly to the skills available in Old School RuneScape.
+ */
+public enum Skill {
+ // Combat skills
+ ATTACK(0, "Attack"),
+ DEFENCE(1, "Defence"),
+ STRENGTH(2, "Strength"),
+ HITPOINTS(3, "Hitpoints"),
+ RANGED(4, "Ranged"),
+ PRAYER(5, "Prayer"),
+ MAGIC(6, "Magic"),
+
+ // Artisan skills
+ COOKING(7, "Cooking"),
+ WOODCUTTING(8, "Woodcutting"),
+ FLETCHING(9, "Fletching"),
+ FISHING(10, "Fishing"),
+ FIREMAKING(11, "Firemaking"),
+ CRAFTING(12, "Crafting"),
+ SMITHING(13, "Smithing"),
+ MINING(14, "Mining"),
+ HERBLORE(15, "Herblore"),
+ AGILITY(16, "Agility"),
+ THIEVING(17, "Thieving"),
+
+ // Support skills
+ SLAYER(18, "Slayer"),
+ FARMING(19, "Farming"),
+ RUNECRAFT(20, "Runecraft"),
+ HUNTER(21, "Hunter"),
+ CONSTRUCTION(22, "Construction");
+
+ private final int index;
+ private final String name;
+
+ Skill(int index, String name) {
+ this.index = index;
+ this.name = name;
+ }
+
+ /**
+ * Get the skill index (used internally by RuneScape).
+ */
+ public int getIndex() {
+ return index;
+ }
+
+ /**
+ * Get the human-readable skill name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Get skill by index.
+ */
+ public static Skill fromIndex(int index) {
+ for (Skill skill : values()) {
+ if (skill.index == index) {
+ return skill;
+ }
+ }
+ throw new IllegalArgumentException("Unknown skill index: " + index);
+ }
+
+ /**
+ * Check if this is a combat skill.
+ */
+ public boolean isCombatSkill() {
+ return this == ATTACK || this == DEFENCE || this == STRENGTH ||
+ this == HITPOINTS || this == RANGED || this == PRAYER || this == MAGIC;
+ }
+
+ /**
+ * Check if this is an artisan skill.
+ */
+ public boolean isArtisanSkill() {
+ return this == COOKING || this == WOODCUTTING || this == FLETCHING ||
+ this == FISHING || this == FIREMAKING || this == CRAFTING ||
+ this == SMITHING || this == MINING || this == HERBLORE ||
+ this == AGILITY || this == THIEVING;
+ }
+
+ /**
+ * Check if this is a support skill.
+ */
+ public boolean isSupportSkill() {
+ return this == SLAYER || this == FARMING || this == RUNECRAFT ||
+ this == HUNTER || this == CONSTRUCTION;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
\ No newline at end of file
diff --git a/modernized-client/src/main/java/com/openosrs/client/core/AgentTestingComponents.java b/modernized-client/src/main/java/com/openosrs/client/core/AgentTestingComponents.java
new file mode 100644
index 0000000..b074f51
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/core/AgentTestingComponents.java
@@ -0,0 +1,425 @@
+package com.openosrs.client.core;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * Additional components for the AgentTestingFramework.
+ */
+
+// ================================
+// SCENARIO SIMULATION
+// ================================
+
+/**
+ * Simulates various game scenarios for testing.
+ */
+class ScenarioSimulator {
+ private static final Logger logger = LoggerFactory.getLogger(ScenarioSimulator.class);
+
+ private final Map handlers;
+
+ public ScenarioSimulator() {
+ this.handlers = new HashMap<>();
+ initializeHandlers();
+ }
+
+ public void initialize() {
+ logger.info("ScenarioSimulator initialized with {} scenario types", handlers.size());
+ }
+
+ public void setupScenario(TestContext context, ScenarioType scenario) {
+ ScenarioHandler handler = handlers.get(scenario);
+ if (handler != null) {
+ handler.setup(context);
+ } else {
+ logger.warn("No handler found for scenario type: {}", scenario);
+ }
+ }
+
+ private void initializeHandlers() {
+ handlers.put(ScenarioType.BASIC_GAMEPLAY, new BasicGameplayHandler());
+ handlers.put(ScenarioType.PERFORMANCE_STRESS, new PerformanceStressHandler());
+ handlers.put(ScenarioType.SCRIPTING_AUTOMATION, new ScriptingAutomationHandler());
+ handlers.put(ScenarioType.INTERFACE_INTERACTION, new InterfaceInteractionHandler());
+ handlers.put(ScenarioType.NETWORK_CONDITIONS, new NetworkConditionsHandler());
+ handlers.put(ScenarioType.MEMORY_PRESSURE, new MemoryPressureHandler());
+ handlers.put(ScenarioType.CONCURRENT_AGENTS, new ConcurrentAgentsHandler());
+ handlers.put(ScenarioType.ERROR_RECOVERY, new ErrorRecoveryHandler());
+ }
+
+ interface ScenarioHandler {
+ void setup(TestContext context);
+ }
+
+ static class BasicGameplayHandler implements ScenarioHandler {
+ @Override
+ public void setup(TestContext context) {
+ context.setData("scenario_type", "basic_gameplay");
+ context.setData("player_level", 50);
+ context.setData("location", "lumbridge");
+ }
+ }
+
+ static class PerformanceStressHandler implements ScenarioHandler {
+ @Override
+ public void setup(TestContext context) {
+ context.setData("scenario_type", "performance_stress");
+ context.setData("event_frequency", "high");
+ context.setData("memory_pressure", true);
+ }
+ }
+
+ static class ScriptingAutomationHandler implements ScenarioHandler {
+ @Override
+ public void setup(TestContext context) {
+ context.setData("scenario_type", "scripting_automation");
+ context.setData("scripting_enabled", true);
+ context.setData("automation_level", "advanced");
+ }
+ }
+
+ static class InterfaceInteractionHandler implements ScenarioHandler {
+ @Override
+ public void setup(TestContext context) {
+ context.setData("scenario_type", "interface_interaction");
+ context.setData("interface_complexity", "high");
+ context.setData("response_delays", true);
+ }
+ }
+
+ static class NetworkConditionsHandler implements ScenarioHandler {
+ @Override
+ public void setup(TestContext context) {
+ context.setData("scenario_type", "network_conditions");
+ context.setData("latency_simulation", 200);
+ context.setData("packet_loss", 0.05);
+ }
+ }
+
+ static class MemoryPressureHandler implements ScenarioHandler {
+ @Override
+ public void setup(TestContext context) {
+ context.setData("scenario_type", "memory_pressure");
+ context.setData("memory_limit", "low");
+ context.setData("gc_frequency", "high");
+ }
+ }
+
+ static class ConcurrentAgentsHandler implements ScenarioHandler {
+ @Override
+ public void setup(TestContext context) {
+ context.setData("scenario_type", "concurrent_agents");
+ context.setData("agent_count", 5);
+ context.setData("resource_contention", true);
+ }
+ }
+
+ static class ErrorRecoveryHandler implements ScenarioHandler {
+ @Override
+ public void setup(TestContext context) {
+ context.setData("scenario_type", "error_recovery");
+ context.setData("error_injection", true);
+ context.setData("recovery_testing", true);
+ }
+ }
+}
+
+// ================================
+// AGENT VALIDATION
+// ================================
+
+/**
+ * Validates agent behavior and performance.
+ */
+class AgentValidator {
+ private static final Logger logger = LoggerFactory.getLogger(AgentValidator.class);
+
+ public boolean validateResult(TestContext context, TestExecutionResult result) {
+ if (!result.isSuccess()) {
+ return false;
+ }
+
+ // Scenario-specific validation
+ String scenarioType = (String) context.getData("scenario_type");
+ if (scenarioType != null) {
+ return validateScenarioSpecific(context, result, scenarioType);
+ }
+
+ return true;
+ }
+
+ private boolean validateScenarioSpecific(TestContext context, TestExecutionResult result, String scenarioType) {
+ switch (scenarioType) {
+ case "performance_stress":
+ return validatePerformance(context, result);
+ case "memory_pressure":
+ return validateMemoryUsage(context, result);
+ case "network_conditions":
+ return validateNetworkResilience(context, result);
+ default:
+ return true;
+ }
+ }
+
+ private boolean validatePerformance(TestContext context, TestExecutionResult result) {
+ // Check if performance metrics are within acceptable bounds
+ Map details = result.getDetails();
+ if (details.containsKey("execution_time")) {
+ long executionTime = (Long) details.get("execution_time");
+ return executionTime < 5000; // 5 second limit
+ }
+ return true;
+ }
+
+ private boolean validateMemoryUsage(TestContext context, TestExecutionResult result) {
+ // Check memory usage metrics
+ Map details = result.getDetails();
+ if (details.containsKey("memory_used")) {
+ long memoryUsed = (Long) details.get("memory_used");
+ return memoryUsed < 100 * 1024 * 1024; // 100MB limit
+ }
+ return true;
+ }
+
+ private boolean validateNetworkResilience(TestContext context, TestExecutionResult result) {
+ // Check network error handling
+ Map details = result.getDetails();
+ if (details.containsKey("network_errors_handled")) {
+ boolean handled = (Boolean) details.get("network_errors_handled");
+ return handled;
+ }
+ return true;
+ }
+}
+
+// ================================
+// MOCK GAME STATE
+// ================================
+
+/**
+ * Provides isolated game state for testing.
+ */
+class MockGameState {
+ private static final Logger logger = LoggerFactory.getLogger(MockGameState.class);
+
+ private final Map> isolatedStates;
+
+ public MockGameState() {
+ this.isolatedStates = new ConcurrentHashMap<>();
+ }
+
+ public void initialize() {
+ logger.info("MockGameState initialized");
+ }
+
+ public void createIsolatedState(String testId) {
+ Map state = new HashMap<>();
+ state.put("created_at", System.currentTimeMillis());
+ state.put("player_position", Map.of("x", 3200, "y", 3200, "z", 0));
+ state.put("inventory", new ArrayList<>());
+ state.put("bank", new ArrayList<>());
+ state.put("skills", createDefaultSkills());
+
+ isolatedStates.put(testId, state);
+ logger.debug("Created isolated state for test: {}", testId);
+ }
+
+ public void destroyIsolatedState(String testId) {
+ isolatedStates.remove(testId);
+ logger.debug("Destroyed isolated state for test: {}", testId);
+ }
+
+ public void applyConfiguration(String testId, Map config) {
+ Map state = isolatedStates.get(testId);
+ if (state != null) {
+ state.putAll(config);
+ }
+ }
+
+ public void enableScriptingSupport(String testId) {
+ Map state = isolatedStates.get(testId);
+ if (state != null) {
+ state.put("scripting_enabled", true);
+ state.put("script_manager", new MockScriptManager());
+ }
+ }
+
+ public Object getStateValue(String testId, String key) {
+ Map state = isolatedStates.get(testId);
+ return state != null ? state.get(key) : null;
+ }
+
+ private Map createDefaultSkills() {
+ Map skills = new HashMap<>();
+ skills.put("attack", 50);
+ skills.put("strength", 50);
+ skills.put("defence", 50);
+ skills.put("ranged", 50);
+ skills.put("prayer", 50);
+ skills.put("magic", 50);
+ skills.put("woodcutting", 50);
+ skills.put("fishing", 50);
+ skills.put("mining", 50);
+ return skills;
+ }
+
+ static class MockScriptManager {
+ public boolean executeScript(String scriptName) {
+ // Mock script execution
+ return true;
+ }
+ }
+}
+
+// ================================
+// PERFORMANCE MONITORING
+// ================================
+
+/**
+ * Monitors performance metrics during testing.
+ */
+class PerformanceMonitor {
+ private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitor.class);
+
+ private final Map activeMonitors;
+ private final ScheduledExecutorService scheduler;
+
+ public PerformanceMonitor() {
+ this.activeMonitors = new ConcurrentHashMap<>();
+ this.scheduler = Executors.newScheduledThreadPool(2, r -> {
+ Thread t = new Thread(r, "Performance-Monitor");
+ t.setDaemon(true);
+ return t;
+ });
+ }
+
+ public void initialize() {
+ logger.info("PerformanceMonitor initialized");
+ }
+
+ public void startMonitoring(String testId) {
+ PerformanceMetrics metrics = new PerformanceMetrics(testId);
+ activeMonitors.put(testId, metrics);
+
+ // Schedule periodic metric collection
+ scheduler.scheduleAtFixedRate(() -> {
+ collectMetrics(testId);
+ }, 1, 1, TimeUnit.SECONDS);
+
+ logger.debug("Started performance monitoring for test: {}", testId);
+ }
+
+ public void stopMonitoring(String testId) {
+ PerformanceMetrics metrics = activeMonitors.remove(testId);
+ if (metrics != null) {
+ metrics.finalize();
+ }
+ logger.debug("Stopped performance monitoring for test: {}", testId);
+ }
+
+ public PerformanceMetrics getMetrics(String testId) {
+ return activeMonitors.get(testId);
+ }
+
+ public void shutdown() {
+ scheduler.shutdown();
+ try {
+ if (!scheduler.awaitTermination(2, TimeUnit.SECONDS)) {
+ scheduler.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ scheduler.shutdownNow();
+ }
+ }
+
+ private void collectMetrics(String testId) {
+ PerformanceMetrics metrics = activeMonitors.get(testId);
+ if (metrics != null) {
+ Runtime runtime = Runtime.getRuntime();
+ long totalMemory = runtime.totalMemory();
+ long freeMemory = runtime.freeMemory();
+ long usedMemory = totalMemory - freeMemory;
+
+ metrics.recordMemoryUsage(usedMemory);
+ metrics.recordCpuUsage(getCurrentCpuUsage());
+ }
+ }
+
+ private double getCurrentCpuUsage() {
+ // Simplified CPU usage calculation
+ return Math.random() * 100; // Mock value
+ }
+}
+
+/**
+ * Container for performance metrics.
+ */
+class PerformanceMetrics {
+ private final String testId;
+ private final long startTime;
+ private final List memorySnapshots;
+ private final List cpuSnapshots;
+ private final AtomicLong peakMemory;
+ private final AtomicLong totalOperations;
+
+ public PerformanceMetrics(String testId) {
+ this.testId = testId;
+ this.startTime = System.currentTimeMillis();
+ this.memorySnapshots = new ArrayList<>();
+ this.cpuSnapshots = new ArrayList<>();
+ this.peakMemory = new AtomicLong(0);
+ this.totalOperations = new AtomicLong(0);
+ }
+
+ public synchronized void recordMemoryUsage(long memory) {
+ memorySnapshots.add(memory);
+ peakMemory.updateAndGet(current -> Math.max(current, memory));
+ }
+
+ public synchronized void recordCpuUsage(double cpu) {
+ cpuSnapshots.add(cpu);
+ }
+
+ public void incrementOperations() {
+ totalOperations.incrementAndGet();
+ }
+
+ public void finalize() {
+ // Calculate final metrics
+ }
+
+ public String getTestId() { return testId; }
+ public long getStartTime() { return startTime; }
+ public long getDuration() { return System.currentTimeMillis() - startTime; }
+ public long getPeakMemory() { return peakMemory.get(); }
+ public long getTotalOperations() { return totalOperations.get(); }
+
+ public double getAverageMemoryUsage() {
+ synchronized (this) {
+ return memorySnapshots.stream().mapToLong(Long::longValue).average().orElse(0.0);
+ }
+ }
+
+ public double getAverageCpuUsage() {
+ synchronized (this) {
+ return cpuSnapshots.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
+ }
+ }
+}
\ No newline at end of file
diff --git a/modernized-client/src/main/java/com/openosrs/client/core/AgentTestingFramework.java b/modernized-client/src/main/java/com/openosrs/client/core/AgentTestingFramework.java
new file mode 100644
index 0000000..ebab4e2
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/core/AgentTestingFramework.java
@@ -0,0 +1,451 @@
+package com.openosrs.client.core;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Queue;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Comprehensive testing framework for AI agents in the RuneScape environment.
+ * Provides test case execution, performance monitoring, scenario simulation,
+ * and comprehensive reporting capabilities.
+ */
+public class AgentTestingFramework {
+ private static final Logger logger = LoggerFactory.getLogger(AgentTestingFramework.class);
+
+ // Configuration
+ private final Duration defaultTestTimeout = Duration.ofMinutes(5);
+ private final boolean enablePerformanceMonitoring = true;
+ private final int maxConcurrentTests = 10;
+
+ // Core components
+ private final ScenarioSimulator scenarioSimulator;
+ private final AgentValidator agentValidator;
+ private final PerformanceMonitor performanceMonitor;
+ private final MockGameState mockGameState;
+ private final TestReporter testReporter;
+
+ // Test management
+ private final Map testSuites;
+ private final Queue testResults;
+ private final ExecutorService testExecutor;
+ private final Set activeTests;
+ private final AtomicLong testCounter;
+
+ // State
+ private volatile boolean initialized = false;
+
+ /**
+ * Create a new AgentTestingFramework instance.
+ */
+ public AgentTestingFramework() {
+ this.scenarioSimulator = new ScenarioSimulator();
+ this.agentValidator = new AgentValidator();
+ this.performanceMonitor = new PerformanceMonitor();
+ this.mockGameState = new MockGameState();
+ this.testReporter = new TestReporter();
+
+ this.testSuites = new ConcurrentHashMap<>();
+ this.testResults = new ConcurrentLinkedQueue<>();
+ this.testExecutor = Executors.newFixedThreadPool(maxConcurrentTests, r -> {
+ Thread t = new Thread(r, "AgentTest-Worker");
+ t.setDaemon(true);
+ return t;
+ });
+ this.activeTests = ConcurrentHashMap.newKeySet();
+ this.testCounter = new AtomicLong(0);
+ }
+
+ /**
+ * Initialize the testing framework.
+ */
+ public synchronized void initialize() {
+ if (initialized) {
+ logger.warn("Testing framework already initialized");
+ return;
+ }
+
+ try {
+ // Initialize all components
+ scenarioSimulator.initialize();
+ performanceMonitor.initialize();
+ mockGameState.initialize();
+ testReporter.initialize();
+
+ // Create default test suites
+ createDefaultTestSuites();
+
+ initialized = true;
+ logger.info("AgentTestingFramework initialized successfully");
+
+ } catch (Exception e) {
+ logger.error("Failed to initialize testing framework", e);
+ throw new RuntimeException("Testing framework initialization failed", e);
+ }
+ }
+
+ /**
+ * Shutdown the testing framework and cleanup resources.
+ */
+ public synchronized void shutdown() {
+ if (!initialized) {
+ return;
+ }
+
+ try {
+ // Stop all active tests
+ logger.info("Stopping {} active tests", activeTests.size());
+
+ // Shutdown executor
+ testExecutor.shutdown();
+ if (!testExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
+ testExecutor.shutdownNow();
+ }
+
+ // Shutdown components
+ performanceMonitor.shutdown();
+
+ initialized = false;
+ logger.info("AgentTestingFramework shutdown completed");
+
+ } catch (Exception e) {
+ logger.error("Error during framework shutdown", e);
+ }
+ }
+
+ // ================================
+ // TEST EXECUTION
+ // ================================
+
+ /**
+ * Run a single test case asynchronously.
+ */
+ public CompletableFuture runTest(String testName, AgentTestCase testCase) {
+ if (!initialized) {
+ throw new IllegalStateException("Testing framework not initialized");
+ }
+
+ String testId = generateTestId(testName);
+ activeTests.add(testId);
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ logger.info("Starting test: {} (ID: {})", testName, testId);
+
+ TestContext context = new TestContext(testId, testName);
+
+ // Setup test environment
+ setupTestEnvironment(context, testCase);
+
+ // Run pre-test setup
+ if (testCase.getPreTestSetup() != null) {
+ testCase.getPreTestSetup().accept(context);
+ }
+
+ // Execute the actual test
+ TestResult result = executeTest(context, testCase);
+
+ // Run post-test cleanup
+ if (testCase.getPostTestCleanup() != null) {
+ testCase.getPostTestCleanup().accept(context);
+ }
+
+ // Record result
+ testResults.offer(result);
+ testReporter.recordResult(result);
+
+ logger.info("Test completed: {} - {}", testName, result.getStatus());
+ return result;
+
+ } catch (Exception e) {
+ logger.error("Test failed with exception: {}", testName, e);
+ TestResult errorResult = TestResult.failure(testId, testName, "Exception: " + e.getMessage());
+ testResults.offer(errorResult);
+ return errorResult;
+ } finally {
+ activeTests.remove(testId);
+ cleanupTestEnvironment(testId);
+ }
+ }, testExecutor)
+ .orTimeout(testCase.getTimeout().toMillis(), TimeUnit.MILLISECONDS)
+ .exceptionally(throwable -> {
+ if (throwable instanceof TimeoutException) {
+ logger.warn("Test timed out: {}", testName);
+ return TestResult.timeout(testId, testName);
+ } else {
+ logger.error("Test error: {}", testName, throwable);
+ return TestResult.failure(testId, testName, throwable.getMessage());
+ }
+ });
+ }
+
+ /**
+ * Run an entire test suite.
+ */
+ public CompletableFuture runTestSuite(String suiteName) {
+ TestSuite suite = testSuites.get(suiteName);
+ if (suite == null) {
+ throw new IllegalArgumentException("Unknown test suite: " + suiteName);
+ }
+
+ return CompletableFuture.supplyAsync(() -> {
+ logger.info("Running test suite: {} ({} tests)", suiteName, suite.getTestCases().size());
+
+ List> futures = new ArrayList<>();
+ for (Map.Entry entry : suite.getTestCases().entrySet()) {
+ futures.add(runTest(entry.getKey(), entry.getValue()));
+ }
+
+ // Wait for all tests to complete
+ List results = futures.stream()
+ .map(CompletableFuture::join)
+ .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
+
+ TestSuiteResult suiteResult = new TestSuiteResult(suiteName, results);
+ logger.info("Test suite completed: {} - {} passed, {} failed",
+ suiteName, suiteResult.getPassedCount(), suiteResult.getFailedCount());
+
+ return suiteResult;
+ }, testExecutor);
+ }
+
+ /**
+ * Execute the core test logic.
+ */
+ private TestResult executeTest(TestContext context, AgentTestCase testCase) {
+ long startTime = System.currentTimeMillis();
+
+ try {
+ // Set up scenario if specified
+ if (testCase.getScenario() != null) {
+ scenarioSimulator.setupScenario(context, testCase.getScenario());
+ }
+
+ // Execute test assertions
+ TestExecutionResult executionResult = testCase.getTestExecution().apply(context);
+
+ long duration = System.currentTimeMillis() - startTime;
+
+ // Validate results
+ boolean passed = agentValidator.validateResult(context, executionResult);
+
+ if (passed) {
+ return TestResult.success(context.getTestId(), context.getTestName(), duration)
+ .withDetails(executionResult.getDetails());
+ } else {
+ return TestResult.failure(context.getTestId(), context.getTestName(),
+ "Validation failed: " + executionResult.getFailureReason())
+ .withDuration(duration);
+ }
+
+ } catch (Exception e) {
+ long duration = System.currentTimeMillis() - startTime;
+ return TestResult.failure(context.getTestId(), context.getTestName(),
+ "Execution failed: " + e.getMessage())
+ .withDuration(duration);
+ }
+ }
+
+ // ================================
+ // TEST ENVIRONMENT MANAGEMENT
+ // ================================
+
+ /**
+ * Setup isolated test environment.
+ */
+ private void setupTestEnvironment(TestContext context, AgentTestCase testCase) {
+ // Initialize mock game state
+ mockGameState.createIsolatedState(context.getTestId());
+
+ // Configure performance monitoring
+ if (enablePerformanceMonitoring) {
+ performanceMonitor.startMonitoring(context.getTestId());
+ }
+
+ // Apply test-specific configuration
+ if (testCase.getEnvironmentConfig() != null) {
+ mockGameState.applyConfiguration(context.getTestId(), testCase.getEnvironmentConfig());
+ }
+ }
+
+ /**
+ * Cleanup test environment.
+ */
+ private void cleanupTestEnvironment(String testId) {
+ try {
+ mockGameState.destroyIsolatedState(testId);
+ performanceMonitor.stopMonitoring(testId);
+ } catch (Exception e) {
+ logger.warn("Error cleaning up test environment for {}", testId, e);
+ }
+ }
+
+ // ================================
+ // TEST CASE BUILDERS
+ // ================================
+
+ /**
+ * Create a basic agent behavior test.
+ */
+ public AgentTestCase.Builder createBehaviorTest(String name) {
+ return new AgentTestCase.Builder(name)
+ .withTimeout(defaultTestTimeout)
+ .withScenario(ScenarioType.BASIC_GAMEPLAY);
+ }
+
+ /**
+ * Create a performance test for agent efficiency.
+ */
+ public AgentTestCase.Builder createPerformanceTest(String name) {
+ return new AgentTestCase.Builder(name)
+ .withTimeout(Duration.ofMinutes(10))
+ .withScenario(ScenarioType.PERFORMANCE_STRESS)
+ .withEnvironmentConfig(Map.of(
+ "enable_performance_monitoring", true,
+ "detailed_metrics", true
+ ));
+ }
+
+ /**
+ * Create a scripting integration test.
+ */
+ public AgentTestCase.Builder createScriptingTest(String name) {
+ return new AgentTestCase.Builder(name)
+ .withTimeout(Duration.ofMinutes(3))
+ .withScenario(ScenarioType.SCRIPTING_AUTOMATION)
+ .withPreTestSetup(context -> {
+ // Setup scripting environment
+ mockGameState.enableScriptingSupport(context.getTestId());
+ });
+ }
+
+ /**
+ * Create an interface interaction test.
+ */
+ public AgentTestCase.Builder createInterfaceTest(String name) {
+ return new AgentTestCase.Builder(name)
+ .withTimeout(Duration.ofMinutes(2))
+ .withScenario(ScenarioType.INTERFACE_INTERACTION)
+ .withEnvironmentConfig(Map.of(
+ "mock_interfaces", true,
+ "interface_response_delay", 100
+ ));
+ }
+
+ // ================================
+ // UTILITY METHODS
+ // ================================
+
+ /**
+ * Create default test suites for common scenarios.
+ */
+ private void createDefaultTestSuites() {
+ // Basic functionality test suite
+ TestSuite basicSuite = new TestSuite("basic_functionality");
+ basicSuite.addTest("login_test", createBasicLoginTest());
+ basicSuite.addTest("interface_test", createBasicInterfaceTest());
+ basicSuite.addTest("event_test", createBasicEventTest());
+ testSuites.put("basic_functionality", basicSuite);
+
+ // Performance test suite
+ TestSuite performanceSuite = new TestSuite("performance");
+ performanceSuite.addTest("stress_test", createStressTest());
+ performanceSuite.addTest("memory_test", createMemoryTest());
+ testSuites.put("performance", performanceSuite);
+
+ // Automation test suite
+ TestSuite automationSuite = new TestSuite("automation");
+ automationSuite.addTest("scripting_test", createScriptingIntegrationTest());
+ automationSuite.addTest("banking_test", createBankingAutomationTest());
+ testSuites.put("automation", automationSuite);
+ }
+
+ /**
+ * Generate unique test ID.
+ */
+ private String generateTestId(String testName) {
+ return String.format("%s_%d_%d",
+ testName.replaceAll("[^a-zA-Z0-9]", "_"),
+ testCounter.incrementAndGet(),
+ System.currentTimeMillis());
+ }
+
+ // Default test implementations
+ private AgentTestCase createBasicLoginTest() {
+ return createBehaviorTest("basic_login")
+ .withTestExecution(context -> {
+ // Simulate login process
+ return TestExecutionResult.success("Login simulation completed");
+ })
+ .build();
+ }
+
+ private AgentTestCase createBasicInterfaceTest() {
+ return createInterfaceTest("basic_interface")
+ .withTestExecution(context -> {
+ // Test interface interactions
+ return TestExecutionResult.success("Interface interactions verified");
+ })
+ .build();
+ }
+
+ private AgentTestCase createBasicEventTest() {
+ return createBehaviorTest("basic_events")
+ .withTestExecution(context -> {
+ // Test event system
+ return TestExecutionResult.success("Event system verified");
+ })
+ .build();
+ }
+
+ private AgentTestCase createStressTest() {
+ return createPerformanceTest("stress_test")
+ .withTestExecution(context -> {
+ // Simulate high load
+ return TestExecutionResult.success("Stress test completed");
+ })
+ .build();
+ }
+
+ private AgentTestCase createMemoryTest() {
+ return createPerformanceTest("memory_test")
+ .withTestExecution(context -> {
+ // Monitor memory usage
+ return TestExecutionResult.success("Memory usage within limits");
+ })
+ .build();
+ }
+
+ private AgentTestCase createScriptingIntegrationTest() {
+ return createScriptingTest("scripting_integration")
+ .withTestExecution(context -> {
+ // Test script execution
+ return TestExecutionResult.success("Scripting integration verified");
+ })
+ .build();
+ }
+
+ private AgentTestCase createBankingAutomationTest() {
+ return createInterfaceTest("banking_automation")
+ .withTestExecution(context -> {
+ // Test banking automation
+ return TestExecutionResult.success("Banking automation verified");
+ })
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/modernized-client/src/main/java/com/openosrs/client/core/AgentTestingReporting.java b/modernized-client/src/main/java/com/openosrs/client/core/AgentTestingReporting.java
new file mode 100644
index 0000000..adec7c1
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/core/AgentTestingReporting.java
@@ -0,0 +1,261 @@
+package com.openosrs.client.core;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Queue;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.stream.Collectors;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+/**
+ * Test reporting and statistics components.
+ */
+
+// ================================
+// TEST REPORTING
+// ================================
+
+/**
+ * Generates reports and statistics for test results.
+ */
+class TestReporter {
+ private static final Logger logger = LoggerFactory.getLogger(TestReporter.class);
+ private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ public void recordResult(TestResult result) {
+ logger.info("Test result recorded: {} - {} ({}ms)",
+ result.getTestName(), result.getStatus(), result.getDurationMs());
+ }
+
+ public TestStatistics generateStatistics(Queue results) {
+ List resultList = new ArrayList<>(results);
+
+ long totalTests = resultList.size();
+ long passedTests = resultList.stream()
+ .filter(r -> r.getStatus() == TestStatus.PASSED)
+ .count();
+ long failedTests = resultList.stream()
+ .filter(r -> r.getStatus() == TestStatus.FAILED)
+ .count();
+ long timeoutTests = resultList.stream()
+ .filter(r -> r.getStatus() == TestStatus.TIMEOUT)
+ .count();
+
+ double averageDuration = resultList.stream()
+ .filter(r -> r.getDurationMs() > 0)
+ .mapToLong(TestResult::getDurationMs)
+ .average()
+ .orElse(0.0);
+
+ long totalDuration = resultList.stream()
+ .mapToLong(TestResult::getDurationMs)
+ .sum();
+
+ // Group by test name for success rates
+ Map> groupedResults = resultList.stream()
+ .collect(Collectors.groupingBy(TestResult::getTestName));
+
+ Map successRates = new HashMap<>();
+ for (Map.Entry> entry : groupedResults.entrySet()) {
+ String testName = entry.getKey();
+ List testResults = entry.getValue();
+
+ long successful = testResults.stream()
+ .filter(r -> r.getStatus() == TestStatus.PASSED)
+ .count();
+
+ double successRate = testResults.isEmpty() ? 0.0 : (double) successful / testResults.size() * 100.0;
+ successRates.put(testName, successRate);
+ }
+
+ return new TestStatistics(
+ totalTests, passedTests, failedTests, timeoutTests,
+ averageDuration, totalDuration, successRates
+ );
+ }
+
+ public void exportResults(Queue results, String format, String outputPath) {
+ try {
+ switch (format.toLowerCase()) {
+ case "json":
+ exportToJson(results, outputPath);
+ break;
+ case "csv":
+ exportToCsv(results, outputPath);
+ break;
+ case "html":
+ exportToHtml(results, outputPath);
+ break;
+ default:
+ logger.warn("Unsupported export format: {}", format);
+ }
+ } catch (IOException e) {
+ logger.error("Failed to export test results to {}", outputPath, e);
+ }
+ }
+
+ private void exportToJson(Queue results, String outputPath) throws IOException {
+ StringBuilder json = new StringBuilder();
+ json.append("{\n");
+ json.append(" \"test_results\": [\n");
+
+ List resultList = new ArrayList<>(results);
+ for (int i = 0; i < resultList.size(); i++) {
+ TestResult result = resultList.get(i);
+ json.append(" {\n");
+ json.append(String.format(" \"test_id\": \"%s\",\n", result.getTestId()));
+ json.append(String.format(" \"test_name\": \"%s\",\n", result.getTestName()));
+ json.append(String.format(" \"status\": \"%s\",\n", result.getStatus()));
+ json.append(String.format(" \"message\": \"%s\",\n", result.getMessage()));
+ json.append(String.format(" \"duration_ms\": %d,\n", result.getDurationMs()));
+ json.append(String.format(" \"timestamp\": \"%s\"\n", result.getTimestamp().format(TIMESTAMP_FORMAT)));
+ json.append(" }");
+ if (i < resultList.size() - 1) {
+ json.append(",");
+ }
+ json.append("\n");
+ }
+
+ json.append(" ]\n");
+ json.append("}");
+
+ Files.write(Paths.get(outputPath), json.toString().getBytes());
+ logger.info("Test results exported to JSON: {}", outputPath);
+ }
+
+ private void exportToCsv(Queue results, String outputPath) throws IOException {
+ StringBuilder csv = new StringBuilder();
+ csv.append("test_id,test_name,status,message,duration_ms,timestamp\n");
+
+ for (TestResult result : results) {
+ csv.append(String.format("%s,%s,%s,\"%s\",%d,%s\n",
+ result.getTestId(),
+ result.getTestName(),
+ result.getStatus(),
+ result.getMessage().replace("\"", "\\\""),
+ result.getDurationMs(),
+ result.getTimestamp().format(TIMESTAMP_FORMAT)
+ ));
+ }
+
+ Files.write(Paths.get(outputPath), csv.toString().getBytes());
+ logger.info("Test results exported to CSV: {}", outputPath);
+ }
+
+ private void exportToHtml(Queue results, String outputPath) throws IOException {
+ StringBuilder html = new StringBuilder();
+ html.append("\n");
+ html.append("\n\nTest Results \n");
+ html.append("\n\n\n");
+
+ html.append("Test Results Report \n");
+ html.append(String.format("Generated on: %s
\n", LocalDateTime.now().format(TIMESTAMP_FORMAT)));
+
+ TestStatistics stats = generateStatistics(results);
+ html.append("Summary \n");
+ html.append("\n");
+ html.append(String.format("Total Tests: %d \n", stats.getTotalTests()));
+ html.append(String.format("Passed: %d \n", stats.getPassedTests()));
+ html.append(String.format("Failed: %d \n", stats.getFailedTests()));
+ html.append(String.format("Timeouts: %d \n", stats.getTimeoutTests()));
+ html.append(String.format("Average Duration: %.2f ms \n", stats.getAverageDuration()));
+ html.append(" \n");
+
+ html.append("Test Results \n");
+ html.append("\n");
+ html.append("Test Name Status Message Duration (ms) Timestamp \n");
+
+ for (TestResult result : results) {
+ String statusClass = result.getStatus().toString().toLowerCase();
+ html.append(String.format("%s %s %s %d %s \n",
+ result.getTestName(),
+ statusClass,
+ result.getStatus(),
+ result.getMessage(),
+ result.getDurationMs(),
+ result.getTimestamp().format(TIMESTAMP_FORMAT)
+ ));
+ }
+
+ html.append("
\n");
+ html.append("\n");
+
+ Files.write(Paths.get(outputPath), html.toString().getBytes());
+ logger.info("Test results exported to HTML: {}", outputPath);
+ }
+}
+
+// ================================
+// TEST STATISTICS
+// ================================
+
+/**
+ * Comprehensive test statistics.
+ */
+class TestStatistics {
+ private final long totalTests;
+ private final long passedTests;
+ private final long failedTests;
+ private final long timeoutTests;
+ private final double averageDuration;
+ private final long totalDuration;
+ private final Map successRates;
+ private final LocalDateTime generatedAt;
+
+ public TestStatistics(long totalTests, long passedTests, long failedTests, long timeoutTests,
+ double averageDuration, long totalDuration, Map successRates) {
+ this.totalTests = totalTests;
+ this.passedTests = passedTests;
+ this.failedTests = failedTests;
+ this.timeoutTests = timeoutTests;
+ this.averageDuration = averageDuration;
+ this.totalDuration = totalDuration;
+ this.successRates = new HashMap<>(successRates);
+ this.generatedAt = LocalDateTime.now();
+ }
+
+ public long getTotalTests() { return totalTests; }
+ public long getPassedTests() { return passedTests; }
+ public long getFailedTests() { return failedTests; }
+ public long getTimeoutTests() { return timeoutTests; }
+ public double getAverageDuration() { return averageDuration; }
+ public long getTotalDuration() { return totalDuration; }
+ public Map getSuccessRates() { return new HashMap<>(successRates); }
+ public LocalDateTime getGeneratedAt() { return generatedAt; }
+
+ public double getSuccessRate() {
+ return totalTests == 0 ? 0.0 : (double) passedTests / totalTests * 100.0;
+ }
+
+ public double getFailureRate() {
+ return totalTests == 0 ? 0.0 : (double) failedTests / totalTests * 100.0;
+ }
+
+ public double getTimeoutRate() {
+ return totalTests == 0 ? 0.0 : (double) timeoutTests / totalTests * 100.0;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "TestStatistics{total=%d, passed=%d, failed=%d, timeouts=%d, successRate=%.2f%%, avgDuration=%.2fms}",
+ totalTests, passedTests, failedTests, timeoutTests, getSuccessRate(), averageDuration
+ );
+ }
+}
\ No newline at end of file
diff --git a/modernized-client/src/main/java/com/openosrs/client/core/AgentTestingSupport.java b/modernized-client/src/main/java/com/openosrs/client/core/AgentTestingSupport.java
new file mode 100644
index 0000000..a542adc
--- /dev/null
+++ b/modernized-client/src/main/java/com/openosrs/client/core/AgentTestingSupport.java
@@ -0,0 +1,292 @@
+package com.openosrs.client.core;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Supporting classes for the AgentTestingFramework.
+ */
+
+// ================================
+// TEST CASE DEFINITIONS
+// ================================
+
+/**
+ * Represents a single test case for agent behavior validation.
+ */
+class AgentTestCase {
+ private final String name;
+ private final ScenarioType scenario;
+ private final Duration timeout;
+ private final Function testExecution;
+ private final Consumer preTestSetup;
+ private final Consumer postTestCleanup;
+ private final Map environmentConfig;
+
+ private AgentTestCase(Builder builder) {
+ this.name = builder.name;
+ this.scenario = builder.scenario;
+ this.timeout = builder.timeout;
+ this.testExecution = builder.testExecution;
+ this.preTestSetup = builder.preTestSetup;
+ this.postTestCleanup = builder.postTestCleanup;
+ this.environmentConfig = builder.environmentConfig;
+ }
+
+ public String getName() { return name; }
+ public ScenarioType getScenario() { return scenario; }
+ public Duration getTimeout() { return timeout; }
+ public Function getTestExecution() { return testExecution; }
+ public Consumer getPreTestSetup() { return preTestSetup; }
+ public Consumer getPostTestCleanup() { return postTestCleanup; }
+ public Map getEnvironmentConfig() { return environmentConfig; }
+
+ public static class Builder {
+ private final String name;
+ private ScenarioType scenario;
+ private Duration timeout = Duration.ofMinutes(5);
+ private Function testExecution;
+ private Consumer preTestSetup;
+ private Consumer postTestCleanup;
+ private Map environmentConfig = new HashMap<>();
+
+ public Builder(String name) {
+ this.name = name;
+ }
+
+ public Builder withScenario(ScenarioType scenario) {
+ this.scenario = scenario;
+ return this;
+ }
+
+ public Builder withTimeout(Duration timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ public Builder withTestExecution(Function testExecution) {
+ this.testExecution = testExecution;
+ return this;
+ }
+
+ public Builder withPreTestSetup(Consumer preTestSetup) {
+ this.preTestSetup = preTestSetup;
+ return this;
+ }
+
+ public Builder withPostTestCleanup(Consumer postTestCleanup) {
+ this.postTestCleanup = postTestCleanup;
+ return this;
+ }
+
+ public Builder withEnvironmentConfig(Map config) {
+ this.environmentConfig.putAll(config);
+ return this;
+ }
+
+ public AgentTestCase build() {
+ if (testExecution == null) {
+ throw new IllegalStateException("Test execution function is required");
+ }
+ return new AgentTestCase(this);
+ }
+ }
+}
+
+/**
+ * Test execution context providing access to test environment.
+ */
+class TestContext {
+ private final String testId;
+ private final String testName;
+ private final LocalDateTime startTime;
+ private final Map testData;
+
+ public TestContext(String testId, String testName) {
+ this.testId = testId;
+ this.testName = testName;
+ this.startTime = LocalDateTime.now();
+ this.testData = new HashMap<>();
+ }
+
+ public String getTestId() { return testId; }
+ public String getTestName() { return testName; }
+ public LocalDateTime getStartTime() { return startTime; }
+
+ public void setData(String key, Object value) {
+ testData.put(key, value);
+ }
+
+ public Object getData(String key) {
+ return testData.get(key);
+ }
+
+ public Map getAllData() {
+ return new HashMap<>(testData);
+ }
+}
+
+/**
+ * Result of test execution.
+ */
+class TestExecutionResult {
+ private final boolean success;
+ private final String message;
+ private final String failureReason;
+ private final Map details;
+
+ private TestExecutionResult(boolean success, String message, String failureReason, Map details) {
+ this.success = success;
+ this.message = message;
+ this.failureReason = failureReason;
+ this.details = details != null ? details : new HashMap<>();
+ }
+
+ public static TestExecutionResult success(String message) {
+ return new TestExecutionResult(true, message, null, null);
+ }
+
+ public static TestExecutionResult success(String message, Map details) {
+ return new TestExecutionResult(true, message, null, details);
+ }
+
+ public static TestExecutionResult failure(String failureReason) {
+ return new TestExecutionResult(false, null, failureReason, null);
+ }
+
+ public static TestExecutionResult failure(String failureReason, Map details) {
+ return new TestExecutionResult(false, null, failureReason, details);
+ }
+
+ public boolean isSuccess() { return success; }
+ public String getMessage() { return message; }
+ public String getFailureReason() { return failureReason; }
+ public Map getDetails() { return new HashMap<>(details); }
+}
+
+/**
+ * Test result with status and timing information.
+ */
+class TestResult {
+ private final String testId;
+ private final String testName;
+ private final TestStatus status;
+ private final String message;
+ private final long durationMs;
+ private final LocalDateTime timestamp;
+ private final Map details;
+
+ private TestResult(String testId, String testName, TestStatus status, String message, long durationMs, Map details) {
+ this.testId = testId;
+ this.testName = testName;
+ this.status = status;
+ this.message = message;
+ this.durationMs = durationMs;
+ this.timestamp = LocalDateTime.now();
+ this.details = details != null ? details : new HashMap<>();
+ }
+
+ public static TestResult success(String testId, String testName, long durationMs) {
+ return new TestResult(testId, testName, TestStatus.PASSED, "Test passed", durationMs, null);
+ }
+
+ public static TestResult failure(String testId, String testName, String message) {
+ return new TestResult(testId, testName, TestStatus.FAILED, message, 0, null);
+ }
+
+ public static TestResult timeout(String testId, String testName) {
+ return new TestResult(testId, testName, TestStatus.TIMEOUT, "Test timed out", 0, null);
+ }
+
+ public TestResult withDuration(long durationMs) {
+ return new TestResult(testId, testName, status, message, durationMs, details);
+ }
+
+ public TestResult withDetails(Map details) {
+ return new TestResult(testId, testName, status, message, durationMs, details);
+ }
+
+ public String getTestId() { return testId; }
+ public String getTestName() { return testName; }
+ public TestStatus getStatus() { return status; }
+ public String getMessage() { return message; }
+ public long getDurationMs() { return durationMs; }
+ public LocalDateTime getTimestamp() { return timestamp; }
+ public Map getDetails() { return new HashMap<>(details); }
+}
+
+/**
+ * Test suite containing multiple test cases.
+ */
+class TestSuite {
+ private final String name;
+ private final Map testCases;
+
+ public TestSuite(String name) {
+ this.name = name;
+ this.testCases = new HashMap<>();
+ }
+
+ public void addTest(String testName, AgentTestCase testCase) {
+ testCases.put(testName, testCase);
+ }
+
+ public String getName() { return name; }
+ public Map getTestCases() { return new HashMap<>(testCases); }
+}
+
+/**
+ * Result of running an entire test suite.
+ */
+class TestSuiteResult {
+ private final String suiteName;
+ private final List results;
+ private final LocalDateTime timestamp;
+
+ public TestSuiteResult(String suiteName, List results) {
+ this.suiteName = suiteName;
+ this.results = new ArrayList<>(results);
+ this.timestamp = LocalDateTime.now();
+ }
+
+ public String getSuiteName() { return suiteName; }
+ public List getResults() { return new ArrayList<>(results); }
+ public LocalDateTime getTimestamp() { return timestamp; }
+
+ public long getPassedCount() {
+ return results.stream().filter(r -> r.getStatus() == TestStatus.PASSED).count();
+ }
+
+ public long getFailedCount() {
+ return results.stream().filter(r -> r.getStatus() == TestStatus.FAILED).count();
+ }
+
+ public long getTimeoutCount() {
+ return results.stream().filter(r -> r.getStatus() == TestStatus.TIMEOUT).count();
+ }
+}
+
+// ================================
+// ENUMS
+// ================================
+
+enum TestStatus {
+ PASSED, FAILED, TIMEOUT, SKIPPED
+}
+
+enum ScenarioType {
+ BASIC_GAMEPLAY,
+ PERFORMANCE_STRESS,
+ SCRIPTING_AUTOMATION,
+ INTERFACE_INTERACTION,
+ NETWORK_CONDITIONS,
+ MEMORY_PRESSURE,
+ CONCURRENT_AGENTS,
+ ERROR_RECOVERY
+}
\ No newline at end of file
diff --git a/modernized-client/src/main/java/com/openosrs/client/core/ClientConfiguration.java b/modernized-client/src/main/java/com/openosrs/client/core/ClientConfiguration.java
index 67e0b82..57c3fa3 100644
--- a/modernized-client/src/main/java/com/openosrs/client/core/ClientConfiguration.java
+++ b/modernized-client/src/main/java/com/openosrs/client/core/ClientConfiguration.java
@@ -108,4 +108,158 @@ public class ClientConfiguration {
try {
Properties props = new Properties();
props.putAll(settings);
- props.store(Files.newBufferedWriter(configFile), \n \"Modernized OpenOSRS Client Configuration\");\n \n logger.info(\"Configuration saved to {}\", configFile);\n \n } catch (IOException e) {\n logger.error(\"Failed to save configuration to {}\", configFile, e);\n }\n }\n \n // Configuration getters with type conversion\n \n public String getString(String key, String defaultValue) {\n return settings.getOrDefault(key, defaultValue);\n }\n \n public String getString(String key) {\n return settings.get(key);\n }\n \n public int getInt(String key, int defaultValue) {\n try {\n String value = settings.get(key);\n return value != null ? Integer.parseInt(value) : defaultValue;\n } catch (NumberFormatException e) {\n logger.warn(\"Invalid integer value for {}: {}\", key, settings.get(key));\n return defaultValue;\n }\n }\n \n public long getLong(String key, long defaultValue) {\n try {\n String value = settings.get(key);\n return value != null ? Long.parseLong(value) : defaultValue;\n } catch (NumberFormatException e) {\n logger.warn(\"Invalid long value for {}: {}\", key, settings.get(key));\n return defaultValue;\n }\n }\n \n public boolean getBoolean(String key, boolean defaultValue) {\n String value = settings.get(key);\n return value != null ? Boolean.parseBoolean(value) : defaultValue;\n }\n \n public double getDouble(String key, double defaultValue) {\n try {\n String value = settings.get(key);\n return value != null ? Double.parseDouble(value) : defaultValue;\n } catch (NumberFormatException e) {\n logger.warn(\"Invalid double value for {}: {}\", key, settings.get(key));\n return defaultValue;\n }\n }\n \n // Configuration setters\n \n public void setString(String key, String value) {\n if (value != null) {\n settings.put(key, value);\n } else {\n settings.remove(key);\n }\n }\n \n public void setInt(String key, int value) {\n settings.put(key, String.valueOf(value));\n }\n \n public void setLong(String key, long value) {\n settings.put(key, String.valueOf(value));\n }\n \n public void setBoolean(String key, boolean value) {\n settings.put(key, String.valueOf(value));\n }\n \n public void setDouble(String key, double value) {\n settings.put(key, String.valueOf(value));\n }\n \n // Convenience methods for common settings\n \n public int getTargetFPS() {\n return getInt(\"client.fps.target\", 50);\n }\n \n public void setTargetFPS(int fps) {\n setInt(\"client.fps.target\", fps);\n }\n \n public boolean isLowMemoryMode() {\n return getBoolean(\"client.memory.lowMemoryMode\", false);\n }\n \n public void setLowMemoryMode(boolean enabled) {\n setBoolean(\"client.memory.lowMemoryMode\", enabled);\n }\n \n public boolean isDebugEnabled() {\n return getBoolean(\"client.debug.enabled\", false);\n }\n \n public void setDebugEnabled(boolean enabled) {\n setBoolean(\"client.debug.enabled\", enabled);\n }\n \n public boolean arePluginsEnabled() {\n return getBoolean(\"client.plugins.enabled\", true);\n }\n \n public void setPluginsEnabled(boolean enabled) {\n setBoolean(\"client.plugins.enabled\", enabled);\n }\n \n public boolean isScriptingEnabled() {\n return getBoolean(\"client.scripting.enabled\", true);\n }\n \n public void setScriptingEnabled(boolean enabled) {\n setBoolean(\"client.scripting.enabled\", enabled);\n }\n \n public int getNetworkTimeout() {\n return getInt(\"network.timeout.ms\", 5000);\n }\n \n public void setNetworkTimeout(int timeoutMs) {\n setInt(\"network.timeout.ms\", timeoutMs);\n }\n \n public int getNetworkMaxRetries() {\n return getInt(\"network.maxRetries\", 3);\n }\n \n public void setNetworkMaxRetries(int maxRetries) {\n setInt(\"network.maxRetries\", maxRetries);\n }\n \n public int getAgentApiTimeout() {\n return getInt(\"agent.api.asyncTimeout.ms\", 1000);\n }\n \n public void setAgentApiTimeout(int timeoutMs) {\n setInt(\"agent.api.asyncTimeout.ms\", timeoutMs);\n }\n \n public boolean isAgentApiStatisticsEnabled() {\n return getBoolean(\"agent.api.enableStatistics\", true);\n }\n \n public void setAgentApiStatisticsEnabled(boolean enabled) {\n setBoolean(\"agent.api.enableStatistics\", enabled);\n }\n}"
\ No newline at end of file
+ props.store(Files.newBufferedWriter(configFile),
+ "Modernized OpenOSRS Client Configuration");
+
+ logger.info("Configuration saved to {}", configFile);
+
+ } catch (IOException e) {
+ logger.error("Failed to save configuration to {}", configFile, e);
+ }
+ }
+
+ // Configuration getters with type conversion
+
+ public String getString(String key, String defaultValue) {
+ return settings.getOrDefault(key, defaultValue);
+ }
+
+ public String getString(String key) {
+ return settings.get(key);
+ }
+
+ public int getInt(String key, int defaultValue) {
+ try {
+ String value = settings.get(key);
+ return value != null ? Integer.parseInt(value) : defaultValue;
+ } catch (NumberFormatException e) {
+ logger.warn("Invalid integer value for {}: {}", key, settings.get(key));
+ return defaultValue;
+ }
+ }
+
+ public long getLong(String key, long defaultValue) {
+ try {
+ String value = settings.get(key);
+ return value != null ? Long.parseLong(value) : defaultValue;
+ } catch (NumberFormatException e) {
+ logger.warn("Invalid long value for {}: {}", key, settings.get(key));
+ return defaultValue;
+ }
+ }
+
+ public boolean getBoolean(String key, boolean defaultValue) {
+ String value = settings.get(key);
+ return value != null ? Boolean.parseBoolean(value) : defaultValue;
+ }
+
+ public double getDouble(String key, double defaultValue) {
+ try {
+ String value = settings.get(key);
+ return value != null ? Double.parseDouble(value) : defaultValue;
+ } catch (NumberFormatException e) {
+ logger.warn("Invalid double value for {}: {}", key, settings.get(key));
+ return defaultValue;
+ }
+ }
+
+ // Configuration setters
+
+ public void setString(String key, String value) {
+ if (value != null) {
+ settings.put(key, value);
+ } else {
+ settings.remove(key);
+ }
+ }
+
+ public void setInt(String key, int value) {
+ settings.put(key, String.valueOf(value));
+ }
+
+ public void setLong(String key, long value) {
+ settings.put(key, String.valueOf(value));
+ }
+
+ public void setBoolean(String key, boolean value) {
+ settings.put(key, String.valueOf(value));
+ }
+
+ public void setDouble(String key, double value) {
+ settings.put(key, String.valueOf(value));
+ }
+
+ // Convenience methods for common settings
+
+ public int getTargetFPS() {
+ return getInt("client.fps.target", 50);
+ }
+
+ public void setTargetFPS(int fps) {
+ setInt("client.fps.target", fps);
+ }
+
+ public boolean isLowMemoryMode() {
+ return getBoolean("client.memory.lowMemoryMode", false);
+ }
+
+ public void setLowMemoryMode(boolean enabled) {
+ setBoolean("client.memory.lowMemoryMode", enabled);
+ }
+
+ public boolean isDebugEnabled() {
+ return getBoolean("client.debug.enabled", false);
+ }
+
+ public void setDebugEnabled(boolean enabled) {
+ setBoolean("client.debug.enabled", enabled);
+ }
+
+ public boolean arePluginsEnabled() {
+ return getBoolean("client.plugins.enabled", true);
+ }
+
+ public void setPluginsEnabled(boolean enabled) {
+ setBoolean("client.plugins.enabled", enabled);
+ }
+
+ public boolean isScriptingEnabled() {
+ return getBoolean("client.scripting.enabled", true);
+ }
+
+ public void setScriptingEnabled(boolean enabled) {
+ setBoolean("client.scripting.enabled", enabled);
+ }
+
+ public int getNetworkTimeout() {
+ return getInt("network.timeout.ms", 5000);
+ }
+
+ public void setNetworkTimeout(int timeoutMs) {
+ setInt("network.timeout.ms", timeoutMs);
+ }
+
+ public int getNetworkMaxRetries() {
+ return getInt("network.maxRetries", 3);
+ }
+
+ public void setNetworkMaxRetries(int maxRetries) {
+ setInt("network.maxRetries", maxRetries);
+ }
+
+ public int getAgentApiTimeout() {
+ return getInt("agent.api.asyncTimeout.ms", 1000);
+ }
+
+ public void setAgentApiTimeout(int timeoutMs) {
+ setInt("agent.api.asyncTimeout.ms", timeoutMs);
+ }
+
+ public boolean isAgentApiStatisticsEnabled() {
+ return getBoolean("agent.api.enableStatistics", true);
+ }
+
+ public void setAgentApiStatisticsEnabled(boolean enabled) {
+ setBoolean("agent.api.enableStatistics", enabled);
+ }
+}
\ No newline at end of file
diff --git a/modernized-client/src/main/java/com/openosrs/client/core/ClientCore.java b/modernized-client/src/main/java/com/openosrs/client/core/ClientCore.java
index e6d8cd6..99f451d 100644
--- a/modernized-client/src/main/java/com/openosrs/client/core/ClientCore.java
+++ b/modernized-client/src/main/java/com/openosrs/client/core/ClientCore.java
@@ -1,21 +1,8 @@
- /**
- * Set the RuneLite client instance for bridge integration.
- */
- public void setRuneLiteClient(Client client) {
- runeLiteBridge.setRuneLiteClient(client);
- logger.info("RuneLite client integration established");
- }
-
- /**
- * Check if RuneLite bridge is active.
- */
- public boolean isBridgeActive() {
- return runeLiteBridge.isActive();
- }package com.openosrs.client.core;
+package com.openosrs.client.core;
import com.openosrs.client.core.bridge.RuneLiteBridge;
import com.openosrs.client.core.bridge.BridgeAdapter;
-import com.openosrs.client.core.state.*;
+
import net.runelite.api.Client;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,22 +13,22 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* ClientCore - Central hub for managing game state and core client functionality.
- *
+ *
* This class maintains the essential game state that agents need access to:
* - Player state and position
* - World objects and NPCs
* - Inventory and equipment
* - Game settings and preferences
- *
+ *
* All state is designed to be thread-safe for concurrent agent access.
*/
public class ClientCore {
private static final Logger logger = LoggerFactory.getLogger(ClientCore.class);
-
+
// Core state management
private final AtomicBoolean initialized = new AtomicBoolean(false);
private final AtomicInteger gameState = new AtomicInteger(GameStateConstants.STARTUP);
-
+
// Game world state
private final WorldState worldState;
private final PlayerState playerState;
@@ -50,27 +37,27 @@ public class ClientCore {
private final NetworkState networkState;
private final LoginState loginState;
private final LoginScreen loginScreen;
-
+
// Bridge components for RuneLite integration
private final RuneLiteBridge runeLiteBridge;
private final BridgeAdapter bridgeAdapter;
-
+
// Configuration and settings
private final ClientConfiguration configuration;
-
+
// Event system for notifying agents of state changes
private final EventSystem eventSystem;
-
+
public ClientCore() {
logger.debug("Initializing ClientCore");
-
+
this.configuration = new ClientConfiguration();
this.eventSystem = new EventSystem();
-
+
// Initialize bridge components
this.runeLiteBridge = new RuneLiteBridge(this);
this.bridgeAdapter = new BridgeAdapter(this, runeLiteBridge);
-
+
// Initialize state managers
this.worldState = new WorldState(eventSystem);
this.playerState = new PlayerState(eventSystem);
@@ -79,10 +66,10 @@ public class ClientCore {
this.networkState = new NetworkState(eventSystem);
this.loginState = new LoginState(eventSystem);
this.loginScreen = new LoginScreen(this);
-
+
logger.debug("ClientCore components created");
}
-
+
/**
* Initialize the client core and all subsystems.
*/
@@ -91,13 +78,13 @@ public class ClientCore {
logger.warn("ClientCore already initialized");
return;
}
-
+
logger.info("Initializing ClientCore subsystems");
-
+
try {
// Initialize configuration
configuration.load();
-
+
// Initialize state managers
worldState.initialize();
playerState.initialize();
@@ -106,21 +93,21 @@ public class ClientCore {
networkState.initialize();
loginState.initialize();
loginScreen.initialize();
-
+
// Initialize event system
eventSystem.initialize();
-
+
setGameState(GameStateConstants.INITIALIZED);
initialized.set(true);
-
+
logger.info("ClientCore initialization complete");
-
+
} catch (Exception e) {
logger.error("Failed to initialize ClientCore", e);
throw new RuntimeException("ClientCore initialization failed", e);
}
}
-
+
/**
* Shutdown the client core and clean up resources.
*/
@@ -129,12 +116,12 @@ public class ClientCore {
logger.warn("ClientCore not initialized, nothing to shutdown");
return;
}
-
+
logger.info("Shutting down ClientCore");
-
+
try {
setGameState(GameStateConstants.SHUTTING_DOWN);
-
+
networkState.shutdown();
interfaceState.shutdown();
inventoryState.shutdown();
@@ -143,17 +130,17 @@ public class ClientCore {
loginState.shutdown();
loginScreen.shutdown();
eventSystem.shutdown();
-
+
setGameState(GameStateConstants.SHUTDOWN);
initialized.set(false);
-
+
logger.info("ClientCore shutdown complete");
-
+
} catch (Exception e) {
logger.error("Error during ClientCore shutdown", e);
}
}
-
+
/**
* Update the game state and notify listeners.
*/
@@ -164,21 +151,21 @@ public class ClientCore {
eventSystem.fireGameStateChanged(oldState, newState);
}
}
-
+
/**
* Get the current game state.
*/
public int getGameState() {
return gameState.get();
}
-
+
/**
* Check if the core is initialized and ready for use.
*/
public boolean isInitialized() {
return initialized.get();
}
-
+
// Accessors for state managers
public WorldState getWorldState() { return worldState; }
public PlayerState getPlayerState() { return playerState; }
@@ -189,11 +176,11 @@ public class ClientCore {
public LoginScreen getLoginScreen() { return loginScreen; }
public ClientConfiguration getConfiguration() { return configuration; }
public EventSystem getEventSystem() { return eventSystem; }
-
+
// Bridge accessors
public RuneLiteBridge getRuneLiteBridge() { return runeLiteBridge; }
public BridgeAdapter getBridgeAdapter() { return bridgeAdapter; }
-
+
/**
* Perform a game tick update - called by the game engine.
*/
@@ -201,13 +188,13 @@ public class ClientCore {
if (!initialized.get()) {
return;
}
-
+
try {
// Update bridge from RuneLite if available
if (runeLiteBridge.isActive()) {
bridgeAdapter.updateFromBridge();
}
-
+
// Update all state managers
worldState.tick();
playerState.tick();
@@ -215,15 +202,15 @@ public class ClientCore {
interfaceState.tick();
networkState.tick();
loginState.tick();
-
+
// Fire tick event for agents
eventSystem.fireGameTick();
-
+
} catch (Exception e) {
logger.error("Error during client core tick", e);
}
}
-
+
/**
* Constants for game states.
*/
@@ -240,4 +227,4 @@ public class ClientCore {
public static final int SHUTDOWN = 9;
public static final int ERROR = -1;
}
-}
\ No newline at end of file
+}
diff --git a/modernized-client/src/main/java/com/openosrs/client/core/CoreStates.java b/modernized-client/src/main/java/com/openosrs/client/core/CoreStates.java
index 7d15faf..fc2d587 100644
--- a/modernized-client/src/main/java/com/openosrs/client/core/CoreStates.java
+++ b/modernized-client/src/main/java/com/openosrs/client/core/CoreStates.java
@@ -15,22 +15,22 @@ public class InventoryState {
private static final Logger logger = LoggerFactory.getLogger(InventoryState.class);
private static final int INVENTORY_SIZE = 28;
private static final int EQUIPMENT_SIZE = 14;
-
+
private final EventSystem eventSystem;
private final ReadWriteLock inventoryLock = new ReentrantReadWriteLock();
private final ReadWriteLock equipmentLock = new ReentrantReadWriteLock();
-
+
// Inventory items [slot] = ItemStack
private final ItemStack[] inventory = new ItemStack[INVENTORY_SIZE];
-
- // Equipment items [slot] = ItemStack
+
+ // Equipment items [slot] = ItemStack
private final ItemStack[] equipment = new ItemStack[EQUIPMENT_SIZE];
-
+
public InventoryState(EventSystem eventSystem) {
this.eventSystem = eventSystem;
initializeItems();
}
-
+
private void initializeItems() {
for (int i = 0; i < INVENTORY_SIZE; i++) {
inventory[i] = new ItemStack(-1, 0);
@@ -39,31 +39,31 @@ public class InventoryState {
equipment[i] = new ItemStack(-1, 0);
}
}
-
+
public void initialize() {
logger.debug("Initializing InventoryState");
}
-
+
public void shutdown() {
logger.debug("Shutting down InventoryState");
}
-
+
public void tick() {
// Update inventory state if needed
}
-
+
// Inventory management
public void setInventoryItem(int slot, int itemId, int quantity) {
if (slot < 0 || slot >= INVENTORY_SIZE) {
logger.warn("Invalid inventory slot: {}", slot);
return;
}
-
+
inventoryLock.writeLock().lock();
try {
ItemStack oldItem = inventory[slot];
inventory[slot] = new ItemStack(itemId, quantity);
-
+
if (oldItem.getItemId() != itemId || oldItem.getQuantity() != quantity) {
eventSystem.fireInventoryChanged(slot, itemId, quantity);
logger.debug("Inventory slot {} changed: {} x{}", slot, itemId, quantity);
@@ -72,12 +72,12 @@ public class InventoryState {
inventoryLock.writeLock().unlock();
}
}
-
+
public ItemStack getInventoryItem(int slot) {
if (slot < 0 || slot >= INVENTORY_SIZE) {
return new ItemStack(-1, 0);
}
-
+
inventoryLock.readLock().lock();
try {
return inventory[slot];
@@ -85,7 +85,7 @@ public class InventoryState {
inventoryLock.readLock().unlock();
}
}
-
+
public ItemStack[] getInventory() {
inventoryLock.readLock().lock();
try {
@@ -96,11 +96,11 @@ public class InventoryState {
inventoryLock.readLock().unlock();
}
}
-
+
public boolean hasItem(int itemId) {
return findItemSlot(itemId) != -1;
}
-
+
public int getItemQuantity(int itemId) {
inventoryLock.readLock().lock();
try {
@@ -115,7 +115,7 @@ public class InventoryState {
inventoryLock.readLock().unlock();
}
}
-
+
public int findItemSlot(int itemId) {
inventoryLock.readLock().lock();
try {
@@ -129,7 +129,7 @@ public class InventoryState {
inventoryLock.readLock().unlock();
}
}
-
+
public int getEmptySlots() {
inventoryLock.readLock().lock();
try {
@@ -144,23 +144,23 @@ public class InventoryState {
inventoryLock.readLock().unlock();
}
}
-
+
public boolean isInventoryFull() {
return getEmptySlots() == 0;
}
-
+
// Equipment management
public void setEquipmentItem(int slot, int itemId, int quantity) {
if (slot < 0 || slot >= EQUIPMENT_SIZE) {
logger.warn("Invalid equipment slot: {}", slot);
return;
}
-
+
equipmentLock.writeLock().lock();
try {
ItemStack oldItem = equipment[slot];
equipment[slot] = new ItemStack(itemId, quantity);
-
+
if (oldItem.getItemId() != itemId || oldItem.getQuantity() != quantity) {
// Fire equipment changed event
logger.debug("Equipment slot {} changed: {} x{}", slot, itemId, quantity);
@@ -169,12 +169,12 @@ public class InventoryState {
equipmentLock.writeLock().unlock();
}
}
-
+
public ItemStack getEquipmentItem(int slot) {
if (slot < 0 || slot >= EQUIPMENT_SIZE) {
return new ItemStack(-1, 0);
}
-
+
equipmentLock.readLock().lock();
try {
return equipment[slot];
@@ -182,7 +182,7 @@ public class InventoryState {
equipmentLock.readLock().unlock();
}
}
-
+
public ItemStack[] getEquipment() {
equipmentLock.readLock().lock();
try {
@@ -193,29 +193,29 @@ public class InventoryState {
equipmentLock.readLock().unlock();
}
}
-
+
/**
* Represents an item stack (item ID + quantity).
*/
public static class ItemStack {
private final int itemId;
private final int quantity;
-
+
public ItemStack(int itemId, int quantity) {
this.itemId = itemId;
this.quantity = quantity;
}
-
+
public int getItemId() { return itemId; }
public int getQuantity() { return quantity; }
public boolean isEmpty() { return itemId == -1 || quantity == 0; }
-
+
@Override
public String toString() {
return isEmpty() ? "Empty" : String.format("Item[%d] x%d", itemId, quantity);
}
}
-
+
/**
* Equipment slot constants.
*/
@@ -234,600 +234,4 @@ public class InventoryState {
}
}
-/**
- * InterfaceState - Manages game interface and widget state.
- */
-class InterfaceState {
- private static final Logger logger = LoggerFactory.getLogger(InterfaceState.class);
-
- private final EventSystem eventSystem;
- private final AtomicInteger currentInterfaceId = new AtomicInteger(-1);
- private final AtomicReference