diff --git a/.gitignore b/.gitignore index c207365..a6dd609 100644 --- a/.gitignore +++ b/.gitignore @@ -75,4 +75,5 @@ node_modules/ # Application specific /logs/ /tmp/ -/cache/ \ No newline at end of file +/cache/ +.claude/settings.local.json diff --git a/build.gradle.kts b/build.gradle.kts index d3ae698..fbd46d3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,11 @@ plugins { - kotlin("jvm") version "2.1.0" java `maven-publish` } java { toolchain { - languageVersion.set(JavaLanguageVersion.of(24)) + languageVersion.set(JavaLanguageVersion.of(25)) } } @@ -24,36 +23,22 @@ version = "2.0.0-SNAPSHOT" repositories { mavenLocal() mavenCentral() - maven { - setUrl("https://nexus.botwithus.net/repository/maven-snapshots/") - } } dependencies { - implementation("net.botwithus.api:api:1.+") - implementation("net.botwithus.imgui:imgui:1.+") - implementation("org.projectlombok:lombok:1.18.26") + implementation("com.botwithus:api:1.0-SNAPSHOT") implementation("com.google.code.gson:gson:2.10.1") - - // Logging dependencies implementation("org.slf4j:slf4j-api:2.0.9") + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -publishing { - repositories { - maven { - url = if (project.version.toString().endsWith("SNAPSHOT")) { - uri("https://nexus.botwithus.net/repository/maven-snapshots/") - } else { - uri("https://nexus.botwithus.net/repository/maven-releases/") - } - credentials { - username = System.getenv("MAVEN_REPO_USER") - password = System.getenv("MAVEN_REPO_PASS") - } - } - } +tasks.test { + useJUnitPlatform() +} +publishing { publications { create("mavenJava") { from(components["java"]) @@ -61,12 +46,15 @@ publishing { pom { name.set("BotWithUs XAPI") description.set("Extended API framework for BotWithUs RuneScape 3 bot development") - + properties.set(mapOf( - "maven.compiler.source" to "24", - "maven.compiler.target" to "24" + "maven.compiler.source" to "25", + "maven.compiler.target" to "25" )) } } } -} \ No newline at end of file + repositories { + mavenLocal() + } +} diff --git a/src/main/java/net/botwithus/rs3/minimenu/Interactive.java b/src/main/java/net/botwithus/rs3/minimenu/Interactive.java new file mode 100644 index 0000000..3dc8c6f --- /dev/null +++ b/src/main/java/net/botwithus/rs3/minimenu/Interactive.java @@ -0,0 +1,6 @@ +package net.botwithus.rs3.minimenu; + +public interface Interactive { + int interact(int optionIndex); + int interact(String optionText); +} diff --git a/src/main/java/net/botwithus/scripts/Info.java b/src/main/java/net/botwithus/scripts/Info.java new file mode 100644 index 0000000..edf8e3d --- /dev/null +++ b/src/main/java/net/botwithus/scripts/Info.java @@ -0,0 +1,15 @@ +package net.botwithus.scripts; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Info { + String name() default ""; + String author() default ""; + String version() default "1.0"; + String description() default ""; +} diff --git a/src/main/java/net/botwithus/ui/workspace/ExtInfo.java b/src/main/java/net/botwithus/ui/workspace/ExtInfo.java new file mode 100644 index 0000000..8594d13 --- /dev/null +++ b/src/main/java/net/botwithus/ui/workspace/ExtInfo.java @@ -0,0 +1,12 @@ +package net.botwithus.ui.workspace; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ExtInfo { + String value() default ""; +} diff --git a/src/main/java/net/botwithus/ui/workspace/Workspace.java b/src/main/java/net/botwithus/ui/workspace/Workspace.java new file mode 100644 index 0000000..3e37f49 --- /dev/null +++ b/src/main/java/net/botwithus/ui/workspace/Workspace.java @@ -0,0 +1,13 @@ +package net.botwithus.ui.workspace; + +public class Workspace { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/net/botwithus/ui/workspace/WorkspaceExtension.java b/src/main/java/net/botwithus/ui/workspace/WorkspaceExtension.java new file mode 100644 index 0000000..81e32dc --- /dev/null +++ b/src/main/java/net/botwithus/ui/workspace/WorkspaceExtension.java @@ -0,0 +1,6 @@ +package net.botwithus.ui.workspace; + +public interface WorkspaceExtension { + default void onDraw(Workspace workspace) { + } +} diff --git a/src/main/java/net/botwithus/xapi/XApi.java b/src/main/java/net/botwithus/xapi/XApi.java new file mode 100644 index 0000000..f15805f --- /dev/null +++ b/src/main/java/net/botwithus/xapi/XApi.java @@ -0,0 +1,101 @@ +package net.botwithus.xapi; + +import com.botwithus.bot.api.Client; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.ScriptContext; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public final class XApi { + + private static final ThreadLocal CONTEXT = new ThreadLocal<>(); + private static final ThreadLocal CURRENT_API = new ThreadLocal<>(); + + private XApi() { + } + + public static void bind(ScriptContext context) { + ScriptContext value = Objects.requireNonNull(context, "context"); + CONTEXT.set(value); + CURRENT_API.set(value.getGameAPI()); + } + + public static void bind(GameAPI api) { + CURRENT_API.set(Objects.requireNonNull(api, "api")); + } + + public static void clear() { + CURRENT_API.remove(); + CONTEXT.remove(); + } + + public static ScriptContext context() { + ScriptContext context = CONTEXT.get(); + if (context == null) { + throw new IllegalStateException("No active ScriptContext is bound to the current thread"); + } + return context; + } + + public static GameAPI api() { + GameAPI api = CURRENT_API.get(); + if (api != null) { + return api; + } + return context().getGameAPI(); + } + + public static GameAPI api(String clientName) { + return client(clientName) + .map(Client::getGameAPI) + .orElseThrow(() -> new IllegalArgumentException("Unknown client: " + clientName)); + } + + public static Optional client(String clientName) { + return context().getClientProvider().getClient(clientName); + } + + public static Collection clients() { + return context().getClientProvider().getClients(); + } + + public static Map apis() { + return clients().stream().collect(Collectors.toUnmodifiableMap(Client::getName, Client::getGameAPI)); + } + + public static T using(GameAPI api, Supplier action) { + Objects.requireNonNull(api, "api"); + Objects.requireNonNull(action, "action"); + GameAPI previous = CURRENT_API.get(); + CURRENT_API.set(api); + try { + return action.get(); + } finally { + if (previous == null) { + CURRENT_API.remove(); + } else { + CURRENT_API.set(previous); + } + } + } + + public static void using(GameAPI api, Runnable action) { + using(api, () -> { + action.run(); + return null; + }); + } + + public static com.botwithus.bot.api.event.EventBus events() { + return context().getEventBus(); + } + + public static com.botwithus.bot.api.isc.MessageBus messageBus() { + return context().getMessageBus(); + } +} diff --git a/src/main/java/net/botwithus/xapi/game/BwuWorld.java b/src/main/java/net/botwithus/xapi/game/BwuWorld.java new file mode 100644 index 0000000..2d8bd9f --- /dev/null +++ b/src/main/java/net/botwithus/xapi/game/BwuWorld.java @@ -0,0 +1,48 @@ +package net.botwithus.xapi.game; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.model.LoginState; +import com.botwithus.bot.api.model.World; +import net.botwithus.xapi.XApi; + +import java.util.Collections; +import java.util.List; + +public final class BwuWorld { + + private BwuWorld() { + } + + public static World getCurrent(GameAPI api) { + return api.getCurrentWorld(); + } + + public static World getCurrent() { + return getCurrent(XApi.api()); + } + + public static List getAll(GameAPI api) { + List worlds = api.queryWorlds(true); + return worlds == null ? Collections.emptyList() : worlds; + } + + public static List getAll() { + return getAll(XApi.api()); + } + + public static void hop(GameAPI api, int worldId) { + api.setWorld(worldId); + } + + public static void hop(int worldId) { + hop(XApi.api(), worldId); + } + + public static LoginState getLoginState(GameAPI api) { + return api.getLoginState(); + } + + public static LoginState getLoginState() { + return getLoginState(XApi.api()); + } +} diff --git a/src/main/java/net/botwithus/xapi/game/combat/ActionBar.java b/src/main/java/net/botwithus/xapi/game/combat/ActionBar.java new file mode 100644 index 0000000..db4479d --- /dev/null +++ b/src/main/java/net/botwithus/xapi/game/combat/ActionBar.java @@ -0,0 +1,308 @@ +package net.botwithus.xapi.game.combat; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.inventory.ActionTypes; +import com.botwithus.bot.api.model.Component; +import com.botwithus.bot.api.model.GameAction; +import com.botwithus.bot.api.model.StructType; +import com.botwithus.bot.api.query.ComponentFilter; + +import java.util.List; + +/** + * Action bar utility for finding and using abilities/items on any visible action bar. + *

+ * Supports two lookup strategies: + *

    + *
  • Text-based ({@link #useAbility(GameAPI, String)}) — searches by option text, simpler but less reliable
  • + *
  • Sprite-based ({@link #useAbilityByStruct(GameAPI, int)}) — uses game cache struct → sprite ID, most reliable
  • + *
+ *

+ * Example usage: + *

+ * // Use an ability by name (searches all bars)
+ * ActionBar.useAbility(api, "Soul Sap");
+ *
+ * // Use by struct ID (more reliable, e.g. from NecroAbility enum)
+ * ActionBar.useAbilityByStruct(api, NecroAbility.SOUL_SAP.structId());
+ *
+ * // Check adrenaline
+ * if (ActionBar.hasAdrenaline(api, 50)) { ... }
+ *
+ * // Use an item on the action bar
+ * ActionBar.useItem(api, "Shark", "Eat");
+ * 
+ * + * @see NecroAbility for necromancy-specific ability definitions + */ +public final class ActionBar { + + private ActionBar() {} + + // ── Interface IDs ────────────────────────────────────────────────── + /** Primary action bar interface. */ + public static final int PRIMARY_BAR = 1430; + /** All action bar interfaces (primary + additional bars). */ + public static final int[] ALL_BARS = {1430, 1670, 1671, 1672, 1673}; + + // ── Varps / Varcs ────────────────────────────────────────────────── + /** Varp: which action bar has the queued ability (0 = none). */ + public static final int QUEUED_BAR_VARP = 5861; + /** Varp: slot index of queued ability on the bar. */ + public static final int QUEUED_INDEX_VARP = 4164; + /** Varc: game-cycle tick — changes when an ability fires. */ + public static final int GC_TICK_VARC = 2092; + /** Varp: current adrenaline (0-1000, divide by 10 for %). */ + public static final int ADRENALINE_VARP = 5862; + + // ── Struct param keys ────────────────────────────────────────────── + /** Struct param key for ability sprite ID. */ + public static final String STRUCT_PARAM_SPRITE = "2802"; + /** Struct param key for ability display name. */ + public static final String STRUCT_PARAM_NAME = "2794"; + + // ═══════════════════════════════════════════════════════════════════ + // Ability Usage (text-based) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Uses an ability by name on any action bar. + * Searches all bars for a component whose option matches the ability name. + * + * @param api the game API + * @param abilityName the ability's display name (e.g. "Soul Sap", "Living Death") + * @return true if the ability was found and clicked + */ + public static boolean useAbility(GameAPI api, String abilityName) { + for (int barId : ALL_BARS) { + if (clickAbilityComponent(api, barId, abilityName)) return true; + } + return false; + } + + /** + * Checks whether an ability is on any action bar. + * + * @param api the game API + * @param abilityName the ability's display name + * @return true if found on any bar + */ + public static boolean containsAbility(GameAPI api, String abilityName) { + for (int barId : ALL_BARS) { + if (findAbilityComponent(api, barId, abilityName) != null) return true; + } + return false; + } + + // ═══════════════════════════════════════════════════════════════════ + // Ability Usage (sprite-based — more reliable) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Uses an ability by its struct ID. Reads the sprite ID from the game cache + * and clicks the matching action bar component. + *

+ * This is the most reliable method — sprites are unique per ability and + * are immune to display name changes or text encoding issues. + * + * @param api the game API + * @param structId the ability's struct type ID (e.g. {@code NecroAbility.SOUL_SAP.structId()}) + * @return true if the ability was found and clicked + */ + public static boolean useAbilityByStruct(GameAPI api, int structId) { + Integer spriteId = getSpriteFromStruct(api, structId); + if (spriteId == null) return false; + + for (int barId : ALL_BARS) { + List comps = api.queryComponents( + ComponentFilter.builder() + .interfaceId(barId) + .spriteId(spriteId) + .maxResults(1) + .build()); + if (comps != null && !comps.isEmpty()) { + clickComponent(api, comps.getFirst()); + return true; + } + } + return false; + } + + /** + * Checks whether an ability with the given struct ID is on any action bar. + * + * @param api the game API + * @param structId the ability's struct type ID + * @return true if found on any bar + */ + public static boolean containsAbilityByStruct(GameAPI api, int structId) { + Integer spriteId = getSpriteFromStruct(api, structId); + if (spriteId == null) return false; + + for (int barId : ALL_BARS) { + List comps = api.queryComponents( + ComponentFilter.builder() + .interfaceId(barId) + .spriteId(spriteId) + .maxResults(1) + .build()); + if (comps != null && !comps.isEmpty()) return true; + } + return false; + } + + // ═══════════════════════════════════════════════════════════════════ + // Item Usage + // ═══════════════════════════════════════════════════════════════════ + + /** + * Uses an item on the action bar by name and option (e.g. "Shark", "Eat"). + * Searches all bars. + * + * @param api the game API + * @param itemName the item name (e.g. "Shark", "Saradomin brew flask") + * @param option the interaction option (e.g. "Eat", "Drink") + * @return true if the item component was found and clicked + */ + public static boolean useItem(GameAPI api, String itemName, String option) { + String pattern = "(?i).*" + escapeRegex(option) + ".*" + escapeRegex(itemName) + ".*" + + "|(?i).*" + escapeRegex(itemName) + ".*" + escapeRegex(option) + ".*"; + for (int barId : ALL_BARS) { + List components = api.queryComponents( + ComponentFilter.builder() + .interfaceId(barId) + .optionPattern(pattern) + .maxResults(1) + .build()); + if (components != null && !components.isEmpty()) { + clickComponent(api, components.getFirst()); + return true; + } + } + return false; + } + + /** + * Checks whether an item is on any action bar. + * + * @param api the game API + * @param itemName the item name + * @return true if found on any bar + */ + public static boolean containsItem(GameAPI api, String itemName) { + for (int barId : ALL_BARS) { + List components = api.queryComponents( + ComponentFilter.builder() + .interfaceId(barId) + .optionPattern("(?i).*" + escapeRegex(itemName) + ".*") + .maxResults(1) + .build()); + if (components != null && !components.isEmpty()) return true; + } + return false; + } + + // ═══════════════════════════════════════════════════════════════════ + // Adrenaline + // ═══════════════════════════════════════════════════════════════════ + + /** + * Returns current adrenaline percentage (0-100). + */ + public static int getAdrenaline(GameAPI api) { + return api.getVarp(ADRENALINE_VARP) / 10; + } + + /** + * Returns true if the player has at least the given adrenaline percentage. + */ + public static boolean hasAdrenaline(GameAPI api, int percentRequired) { + return getAdrenaline(api) >= percentRequired; + } + + // ═══════════════════════════════════════════════════════════════════ + // Queued Ability / GC Tick + // ═══════════════════════════════════════════════════════════════════ + + /** + * Returns true if any ability is currently queued. + */ + public static boolean isAbilityQueued(GameAPI api) { + return api.getVarp(QUEUED_BAR_VARP) > 0; + } + + /** + * Returns the current GC tick varc value. + * Useful for detecting when an ability fires (value changes each cast). + */ + public static int getGcTick(GameAPI api) { + return api.getVarcInt(GC_TICK_VARC); + } + + // ═══════════════════════════════════════════════════════════════════ + // Game Cache Helpers + // ═══════════════════════════════════════════════════════════════════ + + /** + * Reads the sprite ID (param 2802) from a struct in the game cache. + * + * @param api the game API + * @param structId the ability's struct type ID + * @return sprite ID, or null if not found + */ + public static Integer getSpriteFromStruct(GameAPI api, int structId) { + StructType struct = api.getStructType(structId); + if (struct == null || struct.params() == null) return null; + Object val = struct.params().get(STRUCT_PARAM_SPRITE); + if (val instanceof Integer i) return i; + if (val instanceof Number n) return n.intValue(); + return null; + } + + /** + * Reads the display name (param 2794) from a struct in the game cache. + * + * @param api the game API + * @param structId the ability's struct type ID + * @return ability name, or null if not found + */ + public static String getNameFromStruct(GameAPI api, int structId) { + StructType struct = api.getStructType(structId); + if (struct == null || struct.params() == null) return null; + Object val = struct.params().get(STRUCT_PARAM_NAME); + return val instanceof String s ? s : null; + } + + // ═══════════════════════════════════════════════════════════════════ + // Internal helpers + // ═══════════════════════════════════════════════════════════════════ + + private static Component findAbilityComponent(GameAPI api, int interfaceId, String abilityName) { + String pattern = "(?i).*" + escapeRegex(abilityName) + ".*"; + List components = api.queryComponents( + ComponentFilter.builder() + .interfaceId(interfaceId) + .optionPattern(pattern) + .maxResults(1) + .build()); + if (components == null || components.isEmpty()) return null; + return components.getFirst(); + } + + private static boolean clickAbilityComponent(GameAPI api, int interfaceId, String abilityName) { + Component comp = findAbilityComponent(api, interfaceId, abilityName); + if (comp == null) return false; + clickComponent(api, comp); + return true; + } + + static void clickComponent(GameAPI api, Component comp) { + int hash = comp.interfaceId() << 16 | comp.componentId(); + api.queueAction(new GameAction( + ActionTypes.COMPONENT, 1, comp.subComponentId(), hash)); + } + + private static String escapeRegex(String input) { + return input.replaceAll("([\\\\\\[\\](){}.*+?^$|])", "\\\\$1"); + } +} diff --git a/src/main/java/net/botwithus/xapi/game/combat/NecroAbility.java b/src/main/java/net/botwithus/xapi/game/combat/NecroAbility.java new file mode 100644 index 0000000..cb45668 --- /dev/null +++ b/src/main/java/net/botwithus/xapi/game/combat/NecroAbility.java @@ -0,0 +1,289 @@ +package net.botwithus.xapi.game.combat; + +import com.botwithus.bot.api.GameAPI; + +import java.util.HashMap; +import java.util.Map; + +/** + * Necromancy ability definitions with cooldown tracking and readiness checks. + *

+ * Each ability stores its struct ID (for game cache / sprite lookup) and a + * VARC pair for cooldown tracking. Cooldowns are measured in client cycles + * (1 cycle = 20ms, so 50 cycles = 1 second). + *

+ * Example usage: + *

+ * // Check if an ability is ready (not on cooldown, GCD clear)
+ * if (NecroAbility.SOUL_SAP.isReady(api)) {
+ *     ActionBar.useAbilityByStruct(api, NecroAbility.SOUL_SAP.structId());
+ * }
+ *
+ * // Check cooldown remaining
+ * int cdMs = NecroAbility.FINGER_OF_DEATH.getCooldownMs(api);
+ *
+ * // Use with NecroRotation for automated combat
+ * NecroRotation rotation = NecroRotation.pvme(api);
+ * rotation.tick(api);
+ * 
+ * + * @see NecroState for necromancy combat state (stacks, conjures, buffs) + * @see NecroRotation for automated rotation execution + */ +public enum NecroAbility { + + // ── Global Cooldown ──────────────────────────────────────────────── + GCD(14881, 2091, 2092, "Global Cooldown", Category.SYSTEM, 0), + + // ── Basic Attacks ────────────────────────────────────────────────── + /** Auto-attack (no cooldown tracking, GCD only). */ + BASIC_ATTACK(48293, 0, 0, "Basic\u00A0Attack", Category.BASIC, 0), + + // ── Basics ───────────────────────────────────────────────────────── + /** Deals 62.4-124.8% damage. Generates 1 residual soul if not on cooldown. */ + TOUCH_OF_DEATH(48296, 7225, 7226, "Touch of Death", Category.BASIC, 0), + /** Deals 20-100% damage. Generates 1 residual soul. Adds 2 necrosis stacks. */ + SOUL_SAP(48298, 7244, 7245, "Soul Sap", Category.BASIC, 0), + /** Deals 44-220% damage (with Soul Sap debuff). Generates 1 residual soul. */ + SOUL_STRIKE(48299, 7247, 7248, "Soul Strike", Category.BASIC, 0), + /** AoE heal — damages enemies, heals you for 150% of damage dealt. */ + BLOOD_SIPHON(48309, 7253, 7254, "Blood Siphon", Category.BASIC, 0), + /** Melee-range AoE. Cycles through 3 tiers for increasing damage. */ + SPECTRAL_SCYTHE(48311, 7255, 7256, "Spectral Scythe", Category.BASIC, 0), + /** Tier 2 Spectral Scythe (unlocked via varbit 11051). */ + SPECTRAL_SCYTHE_T2(48312, 7257, 7258, "Spectral Scythe", Category.BASIC, 0), + /** Tier 3 Spectral Scythe (unlocked via varbit 11054). */ + SPECTRAL_SCYTHE_T3(48313, 7259, 7260, "Spectral Scythe", Category.BASIC, 0), + + // ── Thresholds ───────────────────────────────────────────────────── + /** 283.2-345.6% damage. Consumes necrosis stacks for +10% per 2 stacks. Costs 60% adren. */ + FINGER_OF_DEATH(48297, 7242, 7243, "Finger of Death", Category.THRESHOLD, 60), + /** Launches 5 souls for 5 x (52.8-72%) damage. Requires 5 residual souls. No adren cost. */ + VOLLEY_OF_SOULS(48301, 7249, 7250, "Volley of Souls", Category.THRESHOLD, 0), + /** 40-200% AoE bleed. Blocked after use until internal cooldown clears. Costs 20% adren. */ + BLOAT(48308, 7251, 7252, "Bloat", Category.THRESHOLD, 20), + /** 4 bouncing skulls, each 68.4-342% damage. Costs 60% adren. */ + DEATH_SKULLS(48314, 7261, 7262, "Death Skulls", Category.THRESHOLD, 60), + + // ── Ultimate ─────────────────────────────────────────────────────── + /** 30s buff: basics generate 2x resources, thresholds cost 0 adren. Costs 100% adren. */ + LIVING_DEATH(48324, 7263, 7264, "Living Death", Category.ULTIMATE, 100), + + // ── Conjures ─────────────────────────────────────────────────────── + /** Summons a skeleton warrior. Lasts until dismissed or re-conjured. */ + CONJURE_SKELETON_WARRIOR(48302, 7227, 7228, "Conjure Skeleton Warrior", Category.CONJURE, 0), + /** Summons a putrid zombie. */ + CONJURE_PUTRID_ZOMBIE(48304, 7232, 7233, "Conjure Putrid Zombie", Category.CONJURE, 0), + /** Summons a vengeful ghost. */ + CONJURE_VENGEFUL_GHOST(48306, 7237, 7238, "Conjure Vengeful Ghost", Category.CONJURE, 0), + /** Summons a phantom guardian (blocks some damage). */ + CONJURE_PHANTOM_GUARDIAN(31820, 7791, 7792, "Conjure Phantom Guardian", Category.CONJURE, 0), + + // ── Commands ─────────────────────────────────────────────────────── + /** Commands skeleton: double damage for a period. Requires active skeleton. */ + COMMAND_SKELETON_WARRIOR(48303, 7229, 7230, "Command Skeleton Warrior", Category.COMMAND, 0), + /** Commands zombie: AoE poison attack. Requires active zombie. */ + COMMAND_PUTRID_ZOMBIE(48305, 7234, 7235, "Command Putrid Zombie", Category.COMMAND, 0), + /** Commands ghost: applies Ectoplasmator debuff. Requires active ghost. */ + COMMAND_VENGEFUL_GHOST(48307, 7239, 7240, "Command Vengeful Ghost", Category.COMMAND, 0), + /** Commands phantom: blocks incoming attack. Requires active phantom. */ + COMMAND_PHANTOM_GUARDIAN(32342, 7793, 7794, "Command Phantom Guardian", Category.COMMAND, 0), + + // ── Specials ─────────────────────────────────────────────────────── + /** Next basic kills target below 10% HP and generates 5 souls. 60s cooldown. */ + INVOKE_DEATH(48330, 7275, 7276, "Invoke Death", Category.SPECIAL, 0), + /** AoE damage field around you. Lasts 30s. */ + DARKNESS(48331, 7278, 7279, "Darkness", Category.SPECIAL, 0), + /** Soul Split heals split into damage to target. 30s duration. */ + SPLIT_SOUL(48332, 7281, 7282, "Split Soul", Category.SPECIAL, 0), + /** Links up to 5 targets — share damage. 10s duration. */ + THREADS_OF_FATE(48329, 7272, 7273, "Threads of Fate", Category.SPECIAL, 0), + /** Sacrifices 50% current HP to heal conjures. */ + LIFE_TRANSFER(48328, 7266, 7267, "Life Transfer", Category.SPECIAL, 0), + /** Shield ability — absorbs damage using necrosis stacks. */ + LESSER_BONE_SHIELD(48326, 7268, 7269, "Lesser Bone Shield", Category.SPECIAL, 0), + /** Greater shield ability — stronger absorption. */ + GREATER_BONE_SHIELD(48327, 7270, 7271, "Greater Bone Shield", Category.SPECIAL, 0); + + // ═══════════════════════════════════════════════════════════════════ + // Fields + // ═══════════════════════════════════════════════════════════════════ + + private final int structId; + private final int varc1; + private final int varc2; + private final String displayName; + private final Category category; + private final int adrenalineCost; + + NecroAbility(int structId, int varc1, int varc2, String displayName, Category category, int adrenalineCost) { + this.structId = structId; + this.varc1 = varc1; + this.varc2 = varc2; + this.displayName = displayName; + this.category = category; + this.adrenalineCost = adrenalineCost; + } + + // ═══════════════════════════════════════════════════════════════════ + // Getters + // ═══════════════════════════════════════════════════════════════════ + + /** The ability's struct type ID (used for game cache lookups and sprite-based matching). */ + public int structId() { return structId; } + /** First VARC for cooldown calculation. */ + public int varc1() { return varc1; } + /** Second VARC for cooldown calculation. */ + public int varc2() { return varc2; } + /** Display name as shown in game. */ + public String displayName() { return displayName; } + /** Ability category (basic, threshold, ultimate, etc.). */ + public Category category() { return category; } + /** Adrenaline cost as percentage (0-100). */ + public int adrenalineCost() { return adrenalineCost; } + + // ═══════════════════════════════════════════════════════════════════ + // Cooldown Checks + // ═══════════════════════════════════════════════════════════════════ + + /** + * Returns remaining cooldown in client cycles (1 cycle = 20ms). + * Returns 0 if the ability is ready. + * + * @param api the game API + * @return remaining cooldown in cycles, or 0 if ready + */ + public int getCooldown(GameAPI api) { + if (varc1 == 0 && varc2 == 0) return 0; + int v1 = api.getVarcInt(varc1); + int v2 = api.getVarcInt(varc2); + int base = v2 - v1; + int elapsed = api.getGameCycle() - v1; + return elapsed >= base ? 0 : base - elapsed; + } + + /** + * Returns remaining cooldown in milliseconds. + * + * @param api the game API + * @return remaining cooldown in ms, or 0 if ready + */ + public int getCooldownMs(GameAPI api) { + return getCooldown(api) * 20; + } + + /** + * Returns true if this ability is on cooldown (own cooldown OR GCD). + * + * @param api the game API + * @return true if on any cooldown + */ + public boolean isOnCooldown(GameAPI api) { + if (this == GCD) return getCooldown(api) > 0; + return Math.max(GCD.getCooldown(api), getCooldown(api)) > 0; + } + + /** + * Returns true if only this ability's own cooldown is active (ignores GCD). + * Use this when GCD is tracked externally (e.g. by {@link NecroRotation}). + * + * @param api the game API + * @return true if own cooldown is still ticking + */ + public boolean isOwnCooldownActive(GameAPI api) { + return getCooldown(api) > 0; + } + + /** + * Returns true if this ability is ready to use (no cooldown and no GCD). + * This is the simplest check — use this for manual ability usage. + * + * @param api the game API + * @return true if ready + */ + public boolean isReady(GameAPI api) { + return !isOnCooldown(api); + } + + /** + * Returns true if this ability is ready (own cooldown only, ignores GCD). + * Use this when GCD is tracked externally. + * + * @param api the game API + * @return true if own cooldown is clear + */ + public boolean isReadyIgnoringGcd(GameAPI api) { + if (this == GCD) return getCooldown(api) <= 0; + return !isOwnCooldownActive(api); + } + + /** + * Returns true if the GCD is currently active. + * + * @param api the game API + * @return true if GCD is ticking + */ + public static boolean isGcdActive(GameAPI api) { + return GCD.getCooldown(api) > 0; + } + + // ═══════════════════════════════════════════════════════════════════ + // Lookup + // ═══════════════════════════════════════════════════════════════════ + + private static final Map BY_NAME = new HashMap<>(); + private static final Map BY_STRUCT = new HashMap<>(); + + static { + for (NecroAbility a : values()) { + BY_NAME.putIfAbsent(a.displayName.toLowerCase(), a); + BY_NAME.putIfAbsent(a.name().toLowerCase(), a); + BY_STRUCT.putIfAbsent(a.structId, a); + } + } + + /** + * Finds a necromancy ability by display name (case-insensitive). + * Also matches enum constant names (e.g. "SOUL_SAP"). + * + * @param name ability name + * @return the ability, or null if not found + */ + public static NecroAbility byName(String name) { + if (name == null) return null; + return BY_NAME.get(name.toLowerCase()); + } + + /** + * Finds a necromancy ability by struct ID. + * + * @param structId the struct type ID + * @return the ability, or null if not found + */ + public static NecroAbility byStructId(int structId) { + return BY_STRUCT.get(structId); + } + + // ═══════════════════════════════════════════════════════════════════ + // Category + // ═══════════════════════════════════════════════════════════════════ + + /** + * Necromancy ability categories. + */ + public enum Category { + SYSTEM, + /** Basic abilities (no adrenaline cost, generate adrenaline). */ + BASIC, + /** Threshold abilities (cost adrenaline, strong damage). */ + THRESHOLD, + /** Ultimate abilities (cost 100% adrenaline, very powerful). */ + ULTIMATE, + /** Conjure abilities (summon undead helpers). */ + CONJURE, + /** Command abilities (order conjured undead to perform actions). */ + COMMAND, + /** Special abilities (buffs, debuffs, utility). */ + SPECIAL + } +} diff --git a/src/main/java/net/botwithus/xapi/game/combat/NecroRotation.java b/src/main/java/net/botwithus/xapi/game/combat/NecroRotation.java new file mode 100644 index 0000000..c422aab --- /dev/null +++ b/src/main/java/net/botwithus/xapi/game/combat/NecroRotation.java @@ -0,0 +1,638 @@ +package net.botwithus.xapi.game.combat; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.inventory.ActionTypes; +import com.botwithus.bot.api.model.Component; +import com.botwithus.bot.api.model.GameAction; +import com.botwithus.bot.api.model.StructType; +import com.botwithus.bot.api.query.ComponentFilter; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Necromancy combat rotation manager. Handles ability caching, GCD tracking, + * and automated rotation execution. + *

+ * Quick start — use the built-in PVME rotation: + *

+ * // In onStart():
+ * NecroRotation rotation = NecroRotation.pvme(api);
+ *
+ * // In onLoop() (every tick while in combat):
+ * rotation.onTick();
+ * rotation.execute();
+ * 
+ *

+ * Custom rotation with builder: + *

+ * NecroRotation rotation = NecroRotation.builder(api)
+ *     // Buffs first
+ *     .use(NecroAbility.INVOKE_DEATH).when(NecroState::isInvokeDeathActive, false)
+ *     .use(NecroAbility.SPLIT_SOUL).when(NecroState::isSplitSoulActive, false)
+ *     // Ultimate
+ *     .use(NecroAbility.LIVING_DEATH).whenAdren(100).when(NecroState::isLivingDeathActive, false)
+ *     // Thresholds
+ *     .use(NecroAbility.DEATH_SKULLS).whenAdren(60)
+ *     .use(NecroAbility.FINGER_OF_DEATH).whenAdren(60).whenStacks(4)
+ *     .use(NecroAbility.VOLLEY_OF_SOULS).whenSouls(5)
+ *     .use(NecroAbility.BLOAT).whenAdren(20).when(NecroState::isBloatBlocked, false)
+ *     // Commands
+ *     .commandGhost()
+ *     .commandSkeleton()
+ *     .commandZombie()
+ *     // Basics (fallback)
+ *     .use(NecroAbility.TOUCH_OF_DEATH)
+ *     .use(NecroAbility.SOUL_SAP)
+ *     .use(NecroAbility.SOUL_STRIKE)
+ *     .build();
+ *
+ * // In onLoop():
+ * rotation.onTick();
+ * rotation.execute();
+ * 
+ *

+ * Manual ability usage (no rotation): + *

+ * NecroRotation mgr = new NecroRotation(api);
+ * mgr.cache(); // call once on start
+ *
+ * // Check + use individually
+ * if (mgr.isReady(NecroAbility.SOUL_SAP)) {
+ *     mgr.useAbility(NecroAbility.SOUL_SAP);
+ * }
+ * 
+ * + * @see NecroAbility for ability definitions + * @see NecroState for combat state checks + * @see ActionBar for general action bar utilities + */ +public class NecroRotation { + + /** Default GCD duration in ticks (3 ticks = standard ability). */ + public static final int DEFAULT_GCD = 3; + + /** Action bar interfaces to scan. */ + private static final int[] BAR_INTERFACES = ActionBar.ALL_BARS; + + private final GameAPI api; + + // Cached ability components (resolved during cache()) + private final Map components = new EnumMap<>(NecroAbility.class); + // Special abilities (no enum): display name -> Component + private final Map specialComponents = new HashMap<>(); + + // Rotation steps (set via builder or pvme()) + private List steps = List.of(); + + // GCD tracking + private int gcd = 0; + private boolean cached = false; + + /** + * Creates a new rotation manager. + * Call {@link #cache()} before using abilities. + * + * @param api the game API + */ + public NecroRotation(GameAPI api) { + this.api = api; + } + + // ═══════════════════════════════════════════════════════════════════ + // Factory Methods + // ═══════════════════════════════════════════════════════════════════ + + /** + * Creates a ready-to-use PVME necromancy rotation. + * Automatically caches abilities on creation. + *

+ * Priority: Conjure Army → Invoke Death → Split Soul → Living Death → + * Death Skulls → Finger of Death (12 stacks) → Volley of Souls (5 souls) → + * Bloat → Commands → Life Transfer → Touch of Death → Soul Sap → Soul Strike + * + * @param api the game API + * @return configured rotation, ready for {@link #execute()} + */ + public static NecroRotation pvme(GameAPI api) { + NecroRotation r = new NecroRotation(api); + r.cacheSpecial("Conjure Undead Army"); + + List pvmeSteps = new ArrayList<>(); + + // Conjure upkeep — re-summon when army expires + pvmeSteps.add(Step.special("Conjure Undead Army", + (a, m) -> !NecroState.isGhostSummoned(a) && m.isSpecialCached("Conjure Undead Army"))); + + // Buffs — maintain Invoke Death + Split Soul + pvmeSteps.add(Step.ability(NecroAbility.INVOKE_DEATH, + (a, m) -> m.isReady(NecroAbility.INVOKE_DEATH) && !NecroState.isInvokeDeathActive(a))); + pvmeSteps.add(Step.ability(NecroAbility.SPLIT_SOUL, + (a, m) -> m.isReady(NecroAbility.SPLIT_SOUL) && !NecroState.isSplitSoulActive(a))); + + // Ultimate + pvmeSteps.add(Step.ability(NecroAbility.LIVING_DEATH, + (a, m) -> m.isReady(NecroAbility.LIVING_DEATH) && NecroState.hasAdrenaline(a, 100) + && !NecroState.isLivingDeathActive(a))); + + // Thresholds + pvmeSteps.add(Step.ability(NecroAbility.DEATH_SKULLS, + (a, m) -> m.isReady(NecroAbility.DEATH_SKULLS) && NecroState.hasAdrenaline(a, 60))); + pvmeSteps.add(Step.ability(NecroAbility.FINGER_OF_DEATH, + (a, m) -> m.isReady(NecroAbility.FINGER_OF_DEATH) && NecroState.hasAdrenaline(a, 60) + && NecroState.getNecrosisStacks(a) >= 12)); + pvmeSteps.add(Step.ability(NecroAbility.VOLLEY_OF_SOULS, + (a, m) -> m.isReady(NecroAbility.VOLLEY_OF_SOULS) && NecroState.getResidualSouls(a) >= 5)); + pvmeSteps.add(Step.ability(NecroAbility.BLOAT, + (a, m) -> m.isReady(NecroAbility.BLOAT) && NecroState.hasAdrenaline(a, 20) + && !NecroState.isBloatBlocked(a))); + + // Commands — Ghost > Skeleton > Zombie + pvmeSteps.add(Step.ability(NecroAbility.COMMAND_VENGEFUL_GHOST, + (a, m) -> m.isReady(NecroAbility.COMMAND_VENGEFUL_GHOST) + && NecroState.isGhostSummoned(a) && NecroState.canCommandGhost(a))); + pvmeSteps.add(Step.ability(NecroAbility.COMMAND_SKELETON_WARRIOR, + (a, m) -> m.isReady(NecroAbility.COMMAND_SKELETON_WARRIOR) + && NecroState.isSkeletonSummoned(a) && NecroState.canCommandSkeleton(a))); + pvmeSteps.add(Step.ability(NecroAbility.COMMAND_PUTRID_ZOMBIE, + (a, m) -> m.isReady(NecroAbility.COMMAND_PUTRID_ZOMBIE) + && NecroState.isZombieSummoned(a) && NecroState.canCommandZombie(a))); + + // Life Transfer — heal conjures + pvmeSteps.add(Step.ability(NecroAbility.LIFE_TRANSFER, + (a, m) -> m.isReady(NecroAbility.LIFE_TRANSFER) && NecroState.hasAnySummon(a))); + + // Basics + pvmeSteps.add(Step.whenReady(NecroAbility.TOUCH_OF_DEATH)); + pvmeSteps.add(Step.whenReady(NecroAbility.SOUL_SAP)); + pvmeSteps.add(Step.whenReady(NecroAbility.SOUL_STRIKE)); + + r.steps = List.copyOf(pvmeSteps); + r.cache(); + return r; + } + + /** + * Creates a rotation builder for custom ability priority. + * + * @param api the game API + * @return a new builder + */ + public static Builder builder(GameAPI api) { + return new Builder(api); + } + + // ═══════════════════════════════════════════════════════════════════ + // Caching + // ═══════════════════════════════════════════════════════════════════ + + /** + * Scans all action bars and caches components for necromancy abilities. + * Uses sprite-based lookup (game cache) for reliability, falls back to text. + *

+ * Call this once on script start and whenever the action bar layout changes. + */ + public void cache() { + components.clear(); + + for (NecroAbility ability : NecroAbility.values()) { + if (ability == NecroAbility.GCD) continue; + + // Try sprite-based lookup first + Component comp = findByStruct(ability.structId()); + if (comp == null) { + // Fall back to text + comp = findByText(ability.displayName()); + } + if (comp != null) { + components.put(ability, comp); + } + } + + cached = true; + } + + /** + * Caches a special ability by name (text-based lookup). + * Use for abilities not in the {@link NecroAbility} enum (e.g. "Conjure Undead Army"). + * + * @param abilityName the ability's display name + * @return true if found on an action bar + */ + public boolean cacheSpecial(String abilityName) { + if (specialComponents.containsKey(abilityName)) return true; + Component comp = findByText(abilityName); + if (comp != null) { + specialComponents.put(abilityName, comp); + return true; + } + return false; + } + + /** Returns true if abilities have been cached. */ + public boolean isCached() { return cached; } + + // ═══════════════════════════════════════════════════════════════════ + // Availability Checks + // ═══════════════════════════════════════════════════════════════════ + + /** + * Returns true if the ability is cached (on action bar) and its own + * cooldown is clear. Does not check GCD — that's handled by {@link #execute()}. + * + * @param ability the ability to check + * @return true if ready to use + */ + public boolean isReady(NecroAbility ability) { + return components.containsKey(ability) && ability.isReadyIgnoringGcd(api); + } + + /** + * Returns true if the ability is on an action bar (cached). + */ + public boolean isCachedAbility(NecroAbility ability) { + return components.containsKey(ability); + } + + /** + * Returns true if a special ability (by name) is cached. + */ + public boolean isSpecialCached(String name) { + return specialComponents.containsKey(name); + } + + // ═══════════════════════════════════════════════════════════════════ + // GCD Tracking + // ═══════════════════════════════════════════════════════════════════ + + /** + * Call once per game tick to decrement the GCD counter. + * Must be called in your script's tick handler for rotation timing to work. + */ + public void onTick() { + if (gcd > 0) gcd--; + } + + /** Returns true if the GCD has expired and an ability can fire. */ + public boolean isGcdReady() { return gcd <= 0; } + + /** Returns remaining GCD ticks. */ + public int getGcd() { return gcd; } + + // ═══════════════════════════════════════════════════════════════════ + // Manual Ability Usage + // ═══════════════════════════════════════════════════════════════════ + + /** + * Clicks the cached component for an ability. Does not check GCD. + * + * @param ability the ability to use + * @return true if the component was found and clicked + */ + public boolean useAbility(NecroAbility ability) { + Component comp = components.get(ability); + if (comp == null) return false; + ActionBar.clickComponent(api, comp); + return true; + } + + /** + * Uses an ability with GCD. Only fires if GCD is ready. + * + * @param ability the ability to use + * @return true if fired successfully + */ + public boolean useAbilityWithGcd(NecroAbility ability) { + if (!isGcdReady()) return false; + if (!useAbility(ability)) return false; + gcd = DEFAULT_GCD; + return true; + } + + /** + * Uses a special ability (by name). Does not check GCD. + * + * @param name the ability display name + * @return true if found and clicked + */ + public boolean useSpecial(String name) { + Component comp = specialComponents.get(name); + if (comp == null) return false; + ActionBar.clickComponent(api, comp); + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // Rotation Execution + // ═══════════════════════════════════════════════════════════════════ + + /** + * Executes the configured rotation. Evaluates each step in priority order + * and fires the first one whose condition is met. + *

+ * GCD is enforced automatically — if GCD is still ticking, returns null. + *

+ * Call this in your script's combat loop after {@link #onTick()}. + * + * @return the ability name that was fired, or null if nothing was available + */ + public String execute() { + if (!isGcdReady()) return null; + + for (Step step : steps) { + if (!step.condition.test(api, this)) continue; + + boolean fired; + if (step.ability != null) { + fired = useAbility(step.ability); + } else { + fired = useSpecial(step.name); + } + + if (fired) { + gcd = DEFAULT_GCD; + return step.name; + } + } + return null; + } + + // ═══════════════════════════════════════════════════════════════════ + // Step (internal rotation unit) + // ═══════════════════════════════════════════════════════════════════ + + /** + * A single step in a rotation: an ability + condition that must be met. + */ + record Step(String name, NecroAbility ability, StepCondition condition) { + + static Step whenReady(NecroAbility ability) { + return new Step(ability.displayName(), ability, + (api, mgr) -> mgr.isReady(ability)); + } + + static Step ability(NecroAbility ability, StepCondition condition) { + return new Step(ability.displayName(), ability, condition); + } + + static Step special(String name, StepCondition condition) { + return new Step(name, null, condition); + } + } + + /** + * Condition for a rotation step. + */ + @FunctionalInterface + public interface StepCondition { + boolean test(GameAPI api, NecroRotation mgr); + } + + // ═══════════════════════════════════════════════════════════════════ + // Builder + // ═══════════════════════════════════════════════════════════════════ + + /** + * Fluent builder for custom necromancy rotations. + *

+ * Steps are evaluated in the order they are added — put highest priority + * abilities first (buffs → ultimates → thresholds → commands → basics). + *

+ * Example: + *

+     * NecroRotation rotation = NecroRotation.builder(api)
+     *     .use(NecroAbility.LIVING_DEATH).whenAdren(100).when(NecroState::isLivingDeathActive, false)
+     *     .use(NecroAbility.DEATH_SKULLS).whenAdren(60)
+     *     .use(NecroAbility.FINGER_OF_DEATH).whenAdren(60).whenStacks(4)
+     *     .use(NecroAbility.VOLLEY_OF_SOULS).whenSouls(5)
+     *     .commandGhost()
+     *     .commandSkeleton()
+     *     .use(NecroAbility.TOUCH_OF_DEATH)
+     *     .use(NecroAbility.SOUL_SAP)
+     *     .build();
+     * 
+ */ + public static class Builder { + + private final GameAPI api; + private final List steps = new ArrayList<>(); + private final List specials = new ArrayList<>(); + + // State for the current step being built + private NecroAbility currentAbility; + private String currentSpecialName; + private final List currentConditions = new ArrayList<>(); + + private Builder(GameAPI api) { + this.api = api; + } + + /** + * Adds an ability to the rotation. Subsequent {@code when*} calls + * add conditions to this ability. The step is finalized when the + * next {@code use()} or {@code build()} is called. + */ + public Builder use(NecroAbility ability) { + finalizeCurrentStep(); + currentAbility = ability; + currentSpecialName = null; + // Default condition: ability is ready (cached + off cooldown) + currentConditions.add((a, m) -> m.isReady(ability)); + return this; + } + + /** + * Adds a special ability (by name) to the rotation. + */ + public Builder useSpecial(String name) { + finalizeCurrentStep(); + currentAbility = null; + currentSpecialName = name; + specials.add(name); + currentConditions.add((a, m) -> m.isSpecialCached(name)); + return this; + } + + /** + * Requires minimum adrenaline percentage for the current step. + */ + public Builder whenAdren(int percent) { + currentConditions.add((a, m) -> NecroState.hasAdrenaline(a, percent)); + return this; + } + + /** + * Requires minimum necrosis stacks for the current step. + */ + public Builder whenStacks(int minStacks) { + currentConditions.add((a, m) -> NecroState.getNecrosisStacks(a) >= minStacks); + return this; + } + + /** + * Requires minimum residual souls for the current step. + */ + public Builder whenSouls(int minSouls) { + currentConditions.add((a, m) -> NecroState.getResidualSouls(a) >= minSouls); + return this; + } + + /** + * Adds a custom condition using a {@link NecroState} method. + *

+ * Use {@code expectedValue = false} to require the state to be inactive: + *

+         * .when(NecroState::isLivingDeathActive, false)  // only when Living Death is NOT active
+         * .when(NecroState::isBloatBlocked, false)       // only when Bloat is NOT blocked
+         * 
+ * + * @param stateCheck a method reference like {@code NecroState::isLivingDeathActive} + * @param expectedValue true = require active, false = require inactive + */ + public Builder when(java.util.function.Function stateCheck, boolean expectedValue) { + currentConditions.add((a, m) -> stateCheck.apply(a) == expectedValue); + return this; + } + + /** + * Adds a raw custom condition. + */ + public Builder when(StepCondition condition) { + currentConditions.add(condition); + return this; + } + + /** + * Shorthand: adds Command Vengeful Ghost with proper checks. + */ + public Builder commandGhost() { + finalizeCurrentStep(); + steps.add(Step.ability(NecroAbility.COMMAND_VENGEFUL_GHOST, + (a, m) -> m.isReady(NecroAbility.COMMAND_VENGEFUL_GHOST) + && NecroState.isGhostSummoned(a) && NecroState.canCommandGhost(a))); + return this; + } + + /** + * Shorthand: adds Command Skeleton Warrior with proper checks. + */ + public Builder commandSkeleton() { + finalizeCurrentStep(); + steps.add(Step.ability(NecroAbility.COMMAND_SKELETON_WARRIOR, + (a, m) -> m.isReady(NecroAbility.COMMAND_SKELETON_WARRIOR) + && NecroState.isSkeletonSummoned(a) && NecroState.canCommandSkeleton(a))); + return this; + } + + /** + * Shorthand: adds Command Putrid Zombie with proper checks. + */ + public Builder commandZombie() { + finalizeCurrentStep(); + steps.add(Step.ability(NecroAbility.COMMAND_PUTRID_ZOMBIE, + (a, m) -> m.isReady(NecroAbility.COMMAND_PUTRID_ZOMBIE) + && NecroState.isZombieSummoned(a) && NecroState.canCommandZombie(a))); + return this; + } + + /** + * Shorthand: adds Command Phantom Guardian with proper checks. + */ + public Builder commandPhantom() { + finalizeCurrentStep(); + steps.add(Step.ability(NecroAbility.COMMAND_PHANTOM_GUARDIAN, + (a, m) -> m.isReady(NecroAbility.COMMAND_PHANTOM_GUARDIAN) + && NecroState.isPhantomSummoned(a))); + return this; + } + + /** + * Builds the rotation, caches abilities, and returns a ready-to-use instance. + */ + public NecroRotation build() { + finalizeCurrentStep(); + + NecroRotation rotation = new NecroRotation(api); + rotation.steps = List.copyOf(steps); + + // Cache special abilities + for (String name : specials) { + rotation.cacheSpecial(name); + } + + rotation.cache(); + return rotation; + } + + private void finalizeCurrentStep() { + if (currentAbility == null && currentSpecialName == null) return; + + List conditions = List.copyOf(currentConditions); + StepCondition combined = (a, m) -> { + for (StepCondition c : conditions) { + if (!c.test(a, m)) return false; + } + return true; + }; + + if (currentAbility != null) { + steps.add(Step.ability(currentAbility, combined)); + } else { + steps.add(Step.special(currentSpecialName, combined)); + } + + currentAbility = null; + currentSpecialName = null; + currentConditions.clear(); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // Internal — Component lookup + // ═══════════════════════════════════════════════════════════════════ + + private Component findByStruct(int structId) { + try { + StructType struct = api.getStructType(structId); + if (struct == null || struct.params() == null) return null; + Object val = struct.params().get(ActionBar.STRUCT_PARAM_SPRITE); + int spriteId; + if (val instanceof Integer i) spriteId = i; + else if (val instanceof Number n) spriteId = n.intValue(); + else return null; + + for (int barId : BAR_INTERFACES) { + List comps = api.queryComponents( + ComponentFilter.builder() + .interfaceId(barId) + .spriteId(spriteId) + .maxResults(1) + .build()); + if (comps != null && !comps.isEmpty()) return comps.getFirst(); + } + } catch (Exception ignored) {} + return null; + } + + private Component findByText(String name) { + String pattern = "(?i).*" + escapeRegex(name) + ".*"; + for (int barId : BAR_INTERFACES) { + List comps = api.queryComponents( + ComponentFilter.builder() + .interfaceId(barId) + .optionPattern(pattern) + .maxResults(1) + .build()); + if (comps != null && !comps.isEmpty()) return comps.getFirst(); + } + return null; + } + + private static String escapeRegex(String input) { + return input.replace("\u00A0", "\\s+") + .replaceAll("([\\\\\\[\\](){}.*+?^$|])", "\\\\$1"); + } +} diff --git a/src/main/java/net/botwithus/xapi/game/combat/NecroState.java b/src/main/java/net/botwithus/xapi/game/combat/NecroState.java new file mode 100644 index 0000000..bc3177b --- /dev/null +++ b/src/main/java/net/botwithus/xapi/game/combat/NecroState.java @@ -0,0 +1,247 @@ +package net.botwithus.xapi.game.combat; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.model.Component; +import com.botwithus.bot.api.query.ComponentFilter; + +import java.util.List; + +/** + * Reads necromancy combat state from game variables (varps, varbits, varcs). + *

+ * All methods are static and require a {@link GameAPI} instance. + *

+ * Example usage: + *

+ * // Check resources
+ * int souls = NecroState.getResidualSouls(api);
+ * int stacks = NecroState.getNecrosisStacks(api);
+ *
+ * // Check conjure states
+ * if (NecroState.isGhostSummoned(api) && NecroState.canCommandGhost(api)) {
+ *     ActionBar.useAbilityByStruct(api, NecroAbility.COMMAND_VENGEFUL_GHOST.structId());
+ * }
+ *
+ * // Check active buffs
+ * if (!NecroState.isLivingDeathActive(api) && ActionBar.hasAdrenaline(api, 100)) {
+ *     ActionBar.useAbilityByStruct(api, NecroAbility.LIVING_DEATH.structId());
+ * }
+ *
+ * // Check adrenaline (general combat)
+ * if (NecroState.hasAdrenaline(api, 60)) { ... }
+ * 
+ * + * @see NecroAbility for ability definitions and cooldowns + * @see NecroRotation for automated rotation execution + */ +public final class NecroState { + + private NecroState() {} + + // ═══════════════════════════════════════════════════════════════════ + // Adrenaline + // ═══════════════════════════════════════════════════════════════════ + + /** Varp: adrenaline (0-1000, divide by 10 for %). */ + public static final int ADRENALINE_VARP = 679; + + /** Returns adrenaline percentage (0-100). */ + public static int getAdrenaline(GameAPI api) { + return api.getVarp(ADRENALINE_VARP) / 10; + } + + /** Returns true if adrenaline >= required percentage. */ + public static boolean hasAdrenaline(GameAPI api, int percent) { + return getAdrenaline(api) >= percent; + } + + // ═══════════════════════════════════════════════════════════════════ + // Residual Souls & Necrosis Stacks + // ═══════════════════════════════════════════════════════════════════ + + /** Varp: residual soul count (0-5). */ + public static final int RESIDUAL_SOULS_VARP = 11035; + /** Varp: necrosis stacks on target (0-12). */ + public static final int NECROSIS_STACKS_VARP = 10986; + + /** + * Returns the current residual soul count (0-5). + * Volley of Souls requires 5 souls. + */ + public static int getResidualSouls(GameAPI api) { + return api.getVarp(RESIDUAL_SOULS_VARP); + } + + /** + * Returns necrosis stacks on target (0-12). + * Finger of Death consumes stacks for +10% damage per 2 stacks. + */ + public static int getNecrosisStacks(GameAPI api) { + return api.getVarp(NECROSIS_STACKS_VARP); + } + + // ═══════════════════════════════════════════════════════════════════ + // Conjure State (summoned = varp > 0) + // ═══════════════════════════════════════════════════════════════════ + + public static final int SKELETON_SUMMON_VARP = 10994; + public static final int PHANTOM_SUMMON_VARP = 11000; + public static final int ZOMBIE_SUMMON_VARP = 11006; + public static final int GHOST_SUMMON_VARP = 11018; + + /** Returns true if a skeleton warrior is currently conjured. */ + public static boolean isSkeletonSummoned(GameAPI api) { return api.getVarp(SKELETON_SUMMON_VARP) > 0; } + /** Returns true if a phantom guardian is currently conjured. */ + public static boolean isPhantomSummoned(GameAPI api) { return api.getVarp(PHANTOM_SUMMON_VARP) > 0; } + /** Returns true if a putrid zombie is currently conjured. */ + public static boolean isZombieSummoned(GameAPI api) { return api.getVarp(ZOMBIE_SUMMON_VARP) > 0; } + /** Returns true if a vengeful ghost is currently conjured. */ + public static boolean isGhostSummoned(GameAPI api) { return api.getVarp(GHOST_SUMMON_VARP) > 0; } + + /** Returns true if any conjure is active. */ + public static boolean hasAnySummon(GameAPI api) { + return isSkeletonSummoned(api) || isPhantomSummoned(api) + || isZombieSummoned(api) || isGhostSummoned(api); + } + + // ═══════════════════════════════════════════════════════════════════ + // Command Cooldowns (varp == 0 means ready to command) + // ═══════════════════════════════════════════════════════════════════ + + public static final int COMMAND_SKELETON_VARP = 11002; + public static final int COMMAND_ZOMBIE_VARP = 11009; + public static final int COMMAND_GHOST_VARP = 11021; + + /** Returns true if Command Skeleton Warrior is ready (not on internal cooldown). */ + public static boolean canCommandSkeleton(GameAPI api) { return api.getVarp(COMMAND_SKELETON_VARP) == 0; } + /** Returns true if Command Putrid Zombie is ready. */ + public static boolean canCommandZombie(GameAPI api) { return api.getVarp(COMMAND_ZOMBIE_VARP) == 0; } + /** Returns true if Command Vengeful Ghost is ready. */ + public static boolean canCommandGhost(GameAPI api) { return api.getVarp(COMMAND_GHOST_VARP) == 0; } + + // ═══════════════════════════════════════════════════════════════════ + // Active Buffs / Debuffs + // ═══════════════════════════════════════════════════════════════════ + + /** Varp: Living Death (> -1 = active, counts down remaining ticks). */ + public static final int LIVING_DEATH_VARP = 11059; + /** Varp: Darkness (>= 1 = active). */ + public static final int DARKNESS_VARP = 11074; + /** Varbit: Invoke Death applied to next basic (1 = active). */ + public static final int INVOKE_DEATH_VARBIT = 53247; + /** Varbit: Bloat internal cooldown (1 = blocked, can't use again yet). */ + public static final int BLOAT_VARBIT = 53245; + /** Varp: Bone Shield active (> 0 = absorbing damage). */ + public static final int BONE_SHIELD_VARP = 11212; + /** Varp: Death Spark count (Living Death sparks). */ + public static final int DEATH_SPARK_VARP = 11085; + + /** Returns true if Living Death buff is currently active (30s window). */ + public static boolean isLivingDeathActive(GameAPI api) { return api.getVarp(LIVING_DEATH_VARP) > -1; } + /** Returns true if Darkness is currently active. */ + public static boolean isDarknessActive(GameAPI api) { return api.getVarp(DARKNESS_VARP) >= 1; } + /** Returns true if Invoke Death is applied (next basic will execute). */ + public static boolean isInvokeDeathActive(GameAPI api) { return api.getVarbit(INVOKE_DEATH_VARBIT) == 1; } + /** Returns true if Bloat is on internal cooldown (can't be used). */ + public static boolean isBloatBlocked(GameAPI api) { return api.getVarbit(BLOAT_VARBIT) == 1; } + /** Returns true if Bone Shield is actively absorbing damage. */ + public static boolean isBoneShieldActive(GameAPI api) { return api.getVarp(BONE_SHIELD_VARP) > 0; } + /** Returns the current death spark count. */ + public static int getDeathSparks(GameAPI api) { return api.getVarp(DEATH_SPARK_VARP); } + + // ═══════════════════════════════════════════════════════════════════ + // Spectral Scythe Tier Unlocks + // ═══════════════════════════════════════════════════════════════════ + + public static final int SPECTRAL_SCYTHE_T2_VARBIT = 11051; + public static final int SPECTRAL_SCYTHE_T3_VARBIT = 11054; + + /** Returns true if Spectral Scythe tier 2 is unlocked. */ + public static boolean isSpectralScytheT2Unlocked(GameAPI api) { return api.getVarp(SPECTRAL_SCYTHE_T2_VARBIT) == 1; } + /** Returns true if Spectral Scythe tier 3 is unlocked. */ + public static boolean isSpectralScytheT3Unlocked(GameAPI api) { return api.getVarp(SPECTRAL_SCYTHE_T3_VARBIT) == 1; } + + /** + * Returns the highest unlocked Spectral Scythe tier. + */ + public static NecroAbility getSpectralScytheTier(GameAPI api) { + if (isSpectralScytheT3Unlocked(api)) return NecroAbility.SPECTRAL_SCYTHE_T3; + if (isSpectralScytheT2Unlocked(api)) return NecroAbility.SPECTRAL_SCYTHE_T2; + return NecroAbility.SPECTRAL_SCYTHE; + } + + // ═══════════════════════════════════════════════════════════════════ + // Weapon Special Cooldowns (varc-based, compare to game cycle) + // ═══════════════════════════════════════════════════════════════════ + + /** Varc: Death Grasp / T90 weapon special end cycle. */ + public static final int DEATH_GRASP_VARC = 7285; + /** Varc: Death Essence / T95 weapon special end cycle. */ + public static final int DEATH_ESSENCE_VARC = 7287; + /** Varc: Split Soul active end cycle. */ + public static final int SPLIT_SOUL_VARC = 7283; + /** Varc: Invoke Lord of Bones end cycle. */ + public static final int INVOKE_LORD_VARC = 7349; + + /** Returns true if T90 weapon special (Death Grasp / EoF) is ready. */ + public static boolean isDeathGraspReady(GameAPI api) { + return (api.getVarcInt(DEATH_GRASP_VARC) - api.getGameCycle()) <= 0; + } + + /** Returns true if T95 weapon special (Death Essence) is ready. */ + public static boolean isDeathEssenceReady(GameAPI api) { + return (api.getVarcInt(DEATH_ESSENCE_VARC) - api.getGameCycle()) <= 0; + } + + /** Returns true if Split Soul is currently active (varc-based). */ + public static boolean isSplitSoulActive(GameAPI api) { + return (api.getVarcInt(SPLIT_SOUL_VARC) - api.getGameCycle()) > 0; + } + + /** Returns true if Invoke Lord of Bones is off cooldown. */ + public static boolean isInvokeLordOfBonesReady(GameAPI api) { + return (api.getVarcInt(INVOKE_LORD_VARC) - api.getGameCycle()) <= 0; + } + + // ═══════════════════════════════════════════════════════════════════ + // General Combat State + // ═══════════════════════════════════════════════════════════════════ + + /** Varp that changes when an ability is successfully cast. */ + public static final int ABILITY_CAST_VARP = 4501; + + /** Returns the current ability cast value. Monitor changes to detect successful casts. */ + public static int getAbilityCastValue(GameAPI api) { + return api.getVarp(ABILITY_CAST_VARP); + } + + /** Returns true if any ability is currently queued on an action bar. */ + public static boolean isAbilityQueued(GameAPI api) { + return api.getVarp(ActionBar.QUEUED_BAR_VARP) > 0; + } + + // ═══════════════════════════════════════════════════════════════════ + // Buff Bar (interface 284) — sprite-based detection + // ═══════════════════════════════════════════════════════════════════ + + /** Buff bar interface ID. */ + public static final int BUFF_BAR_INTERFACE = 284; + + /** + * Returns true if a buff with the given sprite ID is visible on the buff bar. + * + * @param api the game API + * @param spriteId the buff's sprite ID + * @return true if the buff is active and visible + */ + public static boolean isBuffActive(GameAPI api, int spriteId) { + List comps = api.queryComponents( + ComponentFilter.builder() + .interfaceId(BUFF_BAR_INTERFACE) + .spriteId(spriteId) + .visibleOnly(true) + .maxResults(1) + .build()); + return comps != null && !comps.isEmpty(); + } +} diff --git a/src/main/java/net/botwithus/xapi/game/entity/BwuPlayer.java b/src/main/java/net/botwithus/xapi/game/entity/BwuPlayer.java index 7961e34..71ac998 100644 --- a/src/main/java/net/botwithus/xapi/game/entity/BwuPlayer.java +++ b/src/main/java/net/botwithus/xapi/game/entity/BwuPlayer.java @@ -1,155 +1,137 @@ package net.botwithus.xapi.game.entity; -import net.botwithus.rs3.cache.assets.vars.VarDomainType; -import net.botwithus.rs3.entities.PathingEntity; -import net.botwithus.rs3.entities.LocalPlayer; -import net.botwithus.rs3.entities.EntityType; -import net.botwithus.rs3.world.World; -import net.botwithus.rs3.client.Client; -// Dialog API usage temporarily commented out for compilation -// import net.botwithus.rs3.game.hud.Dialog; -import net.botwithus.rs3.vars.VarDomain; -import net.botwithus.rs3.world.Locatable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import net.botwithus.rs3.world.Distance; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.model.LocalPlayer; +import net.botwithus.xapi.XApi; import net.botwithus.xapi.util.BwuDistance; +import net.botwithus.xapi.util.position.Positionable; + import java.util.Arrays; import java.util.HashSet; -public class BwuPlayer { - - private static final Logger logger = LoggerFactory.getLogger(BwuPlayer.class); - - /** - * Helper method to get the current target using v2 API - */ - private static PathingEntity getTarget(LocalPlayer player) { - if (player == null) return null; - - EntityType targetType = player.getTargetType(); - int targetIndex = player.getTargetServerIndex(); - - if (targetIndex <= 0) return null; +public final class BwuPlayer { - if (targetType == EntityType.NPC_ENTITY) { - return World.getNpc(targetIndex); - } else if (targetType == EntityType.PLAYER_ENTITY) { - return World.getPlayer(targetIndex); - } + private BwuPlayer() { + } - return null; + public static int getBossKills(GameAPI api) { + return api.getVarp(6437); } public static int getBossKills() { - return VarDomain.getVarValue(6437); + return getBossKills(XApi.api()); } - public static boolean isTargetting(PathingEntity npc) { - var local = LocalPlayer.self(); - if (local == null) - return false; - var target = getTarget(local); - return target != null && target.equals(npc); // && !Dialog.isOpen(); + public static boolean isTargeting(GameAPI api, String... npcName) { + LocalPlayer player = api.getLocalPlayer(); + return player != null && Arrays.stream(npcName).anyMatch(name -> name.equalsIgnoreCase(player.overheadText())); } - public static boolean isTargetting(String... npcName) { - var local = LocalPlayer.self(); - if (local == null) return false; - - PathingEntity target = getTarget(local); - if (target == null) return false; - - String targetName = target.getName(); - return targetName != null && Arrays.asList(npcName).contains(targetName); // && !Dialog.isOpen(); + public static boolean isTargeting(String... npcName) { + return isTargeting(XApi.api(), npcName); } - public static boolean isTargettingNameContaining(String partial) { - partial = partial.toLowerCase(); - var local = LocalPlayer.self(); - if (local == null) return false; - - PathingEntity target = getTarget(local); - if (target == null) return false; - - String targetName = target.getName(); - return targetName != null && targetName.toLowerCase().contains(partial); // && !Dialog.isOpen(); + public static boolean isTargetingNameContaining(GameAPI api, String partial) { + LocalPlayer player = api.getLocalPlayer(); + return player != null && player.overheadText() != null && player.overheadText().toLowerCase().contains(partial.toLowerCase()); } - public static boolean isInAnimation(HashSet animations){ - return isInAnimation(animations, 2000); + public static boolean isTargetingNameContaining(String partial) { + return isTargetingNameContaining(XApi.api(), partial); } - public static boolean isCurrentAnimation(HashSet animations) { - var local = LocalPlayer.self(); - return local != null && animations.contains(local.getAnimationId()); // && !Dialog.isOpen(); + public static boolean isInAnimation(HashSet animations) { + return isCurrentAnimation(XApi.api(), animations); } - public static boolean isInAnimation(HashSet animations, int timeout) { - // TODO: Implement delayUntil equivalent for v2 API - var player = LocalPlayer.self(); - return player != null && animations.contains(player.getAnimationId()); // && !Dialog.isOpen(); + public static boolean isCurrentAnimation(GameAPI api, HashSet animations) { + LocalPlayer player = api.getLocalPlayer(); + return player != null && animations.contains(player.animationId()); } - public static boolean isInAnimation(int[] animationIds, int timeout) { - // TODO: Implement delayUntil equivalent for v2 API - var player = LocalPlayer.self(); - return player != null && Arrays.stream(animationIds).anyMatch(i -> i == player.getAnimationId()); // && !Dialog.isOpen(); + public static boolean isCurrentAnimation(HashSet animations) { + return isCurrentAnimation(XApi.api(), animations); } - public static boolean isAnimating(int timeout) { - // TODO: Implement delayUntil equivalent for v2 API - var player = LocalPlayer.self(); - return player != null && player.getAnimationId() != -1; // && !Dialog.isOpen(); + public static boolean isInInstance(GameAPI api) { + LocalPlayer player = api.getLocalPlayer(); + return player != null && (player.tileX() > 6400 || player.tileY() > 12800); } public static boolean isInInstance() { - try { - var player = LocalPlayer.self(); - if (player == null) { - return false; - } - var pCoord = player.getCoordinate(); - return pCoord.x() > 6400 || pCoord.y() > 12800; - } catch (Exception e) { - logger.info("Error checking if player is in instance: " + e.getMessage()); - return false; - } + return isInInstance(XApi.api()); } - /** - * @return the player's current health percentage 0-100 - */ - public static float getHealthPercent() { - var player = LocalPlayer.self(); - if (player == null) { + public static float getHealthPercent(GameAPI api) { + LocalPlayer player = api.getLocalPlayer(); + if (player == null || player.maxHealth() <= 0) { return 0; } - return ((float) player.getHealth() / (float) player.getMaxHealth()) * 100; + return ((float) player.health() / player.maxHealth()) * 100f; } + public static float getHealthPercent() { + return getHealthPercent(XApi.api()); + } + + public static boolean isPvpEnabled(GameAPI api) { + return api.getVarbit(52975) == 1; + } public static boolean isPvpEnabled() { - return VarDomain.getVarBitValue(52975) == 1; + return isPvpEnabled(XApi.api()); + } + + public static boolean isStunned(GameAPI api) { + return api.getVarcInt(3748) > 0; } public static boolean isStunned() { - return (VarDomain.getVarClient(3748) - Client.getClientCycle()) > 0; + return isStunned(XApi.api()); + } + + public static boolean isPoisoned(GameAPI api) { + return api.getVarcInt(4681) > 0; } public static boolean isPoisoned() { - return (VarDomain.getVarClient(4681) - Client.getClientCycle()) > 0; + return isPoisoned(XApi.api()); + } + + public static boolean isInCombat(GameAPI api) { + return api.getVarbit(1899) != 0; } public static boolean isInCombat() { - return VarDomain.getVarBitValue(1899) != 0; + return isInCombat(XApi.api()); } - public static boolean isLocatableBetweenDestination(Locatable locatable, Locatable destination) { - var player = LocalPlayer.self(); + public static boolean isLocatableBetweenDestination(GameAPI api, Positionable locatable, Positionable destination) { + LocalPlayer player = api.getLocalPlayer(); if (player == null) { return false; } - return BwuDistance.isLocatableBetween(player, locatable, destination) && BwuDistance.isLocatableCloser(player, locatable, destination); + Positionable playerPosition = new Positionable() { + @Override + public int x() { + return player.tileX(); + } + + @Override + public int y() { + return player.tileY(); + } + + @Override + public int plane() { + return player.plane(); + } + }; + return BwuDistance.isLocatableBetween(playerPosition, locatable, destination) + && BwuDistance.isLocatableCloser(playerPosition, locatable, destination); + } + + public static boolean isLocatableBetweenDestination(Positionable locatable, Positionable destination) { + return isLocatableBetweenDestination(XApi.api(), locatable, destination); } -} \ No newline at end of file +} diff --git a/src/main/java/net/botwithus/xapi/game/hud/BwuGrandExchange.java b/src/main/java/net/botwithus/xapi/game/hud/BwuGrandExchange.java new file mode 100644 index 0000000..2af4c89 --- /dev/null +++ b/src/main/java/net/botwithus/xapi/game/hud/BwuGrandExchange.java @@ -0,0 +1,59 @@ +package net.botwithus.xapi.game.hud; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.model.GrandExchangeOffer; +import net.botwithus.xapi.XApi; + +import java.util.Collections; +import java.util.List; + +public final class BwuGrandExchange { + + private static final int GE_INTERFACE_ID = 105; + + private BwuGrandExchange() { + } + + public static boolean isOpen(GameAPI api) { + return api.isInterfaceOpen(GE_INTERFACE_ID); + } + + public static boolean isOpen() { + return isOpen(XApi.api()); + } + + public static List getOffers(GameAPI api) { + List offers = api.getGrandExchangeOffers(); + return offers == null ? Collections.emptyList() : offers; + } + + public static List getOffers() { + return getOffers(XApi.api()); + } + + public static GrandExchangeOffer findOffer(GameAPI api, int itemId) { + return getOffers(api).stream() + .filter(offer -> offer.itemId() == itemId) + .findFirst() + .orElse(null); + } + + public static GrandExchangeOffer findOffer(int itemId) { + return findOffer(XApi.api(), itemId); + } + + public static boolean hasFreeSlot(GameAPI api) { + return getOffers(api).stream().anyMatch(offer -> offer.status() == 0); + } + + public static boolean hasFreeSlot() { + return hasFreeSlot(XApi.api()); + } + + public static int getRemainingQuantity(GrandExchangeOffer offer) { + if (offer == null) { + return 0; + } + return offer.count() - offer.completedCount(); + } +} diff --git a/src/main/java/net/botwithus/xapi/game/hud/Chat.java b/src/main/java/net/botwithus/xapi/game/hud/Chat.java new file mode 100644 index 0000000..07b9413 --- /dev/null +++ b/src/main/java/net/botwithus/xapi/game/hud/Chat.java @@ -0,0 +1,49 @@ +package net.botwithus.xapi.game.hud; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.model.ChatMessage; +import net.botwithus.xapi.XApi; + +import java.util.Collections; +import java.util.List; + +public final class Chat { + + private Chat() { + } + + public static List getHistory(GameAPI api, int messageType, int maxResults) { + List history = api.queryChatHistory(messageType, maxResults); + return history == null ? Collections.emptyList() : history; + } + + public static List getHistory(int messageType, int maxResults) { + return getHistory(XApi.api(), messageType, maxResults); + } + + public static ChatMessage getLastMessage(GameAPI api) { + List history = api.queryChatHistory(-1, 1); + return history == null || history.isEmpty() ? null : history.get(0); + } + + public static ChatMessage getLastMessage() { + return getLastMessage(XApi.api()); + } + + public static ChatMessage getLastMessage(GameAPI api, int messageType) { + List history = api.queryChatHistory(messageType, 1); + return history == null || history.isEmpty() ? null : history.get(0); + } + + public static ChatMessage getLastMessage(int messageType) { + return getLastMessage(XApi.api(), messageType); + } + + public static int getHistorySize(GameAPI api) { + return api.getChatHistorySize(); + } + + public static int getHistorySize() { + return getHistorySize(XApi.api()); + } +} diff --git a/src/main/java/net/botwithus/xapi/game/hud/Dialog.java b/src/main/java/net/botwithus/xapi/game/hud/Dialog.java index 2184ec5..e2edadc 100644 --- a/src/main/java/net/botwithus/xapi/game/hud/Dialog.java +++ b/src/main/java/net/botwithus/xapi/game/hud/Dialog.java @@ -1,163 +1,139 @@ package net.botwithus.xapi.game.hud; -import net.botwithus.rs3.interfaces.Component; -import net.botwithus.rs3.interfaces.ComponentType; -import net.botwithus.rs3.interfaces.Interfaces; -import net.botwithus.rs3.minimenu.MiniMenu; -import net.botwithus.rs3.minimenu.Action; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.inventory.ActionTypes; +import com.botwithus.bot.api.model.Component; +import com.botwithus.bot.api.model.GameAction; +import net.botwithus.xapi.XApi; import net.botwithus.xapi.query.ComponentQuery; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; -/** - * The Dialog class provides methods to interact with the dialogue interfaces in the game. - * It allows checking if any dialogue interface is open, selecting options, retrieving options, - * and interacting with the dialogue based on the current interface. - */ -public class Dialog { - /** - * Checks if any of the dialogue interfaces are open. - * - * @return True if any of the dialogue interfaces (1184, 1186, 1188, 1189, 1191) are open, false otherwise. - */ +public final class Dialog { + + private static final int[] DIALOG_INTERFACES = {1184, 1186, 1188, 1189, 1191}; + private static final int[] OPTION_HASHES = {77856776, 77856781, 77856786, 77856791, 77856796}; + + private Dialog() { + } + + public static boolean isOpen(GameAPI api) { + for (int interfaceId : DIALOG_INTERFACES) { + if (api.isInterfaceOpen(interfaceId)) { + return true; + } + } + return false; + } + public static boolean isOpen() { - return Interfaces.isOpen(1184) || Interfaces.isOpen(1186) || Interfaces.isOpen(1188) || Interfaces.isOpen(1189) || Interfaces.isOpen(1191); + return isOpen(XApi.api()); + } + + public static boolean select(GameAPI api) { + if (!isOpen(api)) { + return false; + } + api.queueAction(new GameAction(ActionTypes.DIALOGUE, 0, -1, resolveDefaultHash(api))); + return true; } - /** - * Checks if the dialogue interface is open and selects the appropriate option based on the current interface. - * - * @return True if the interaction was successful, false otherwise. - */ public static boolean select() { - if (Interfaces.isOpen(1184)) { - return MiniMenu.doAction(Action.DIALOGUE, 0, -1, 77594639) > 0; - } else if (Interfaces.isOpen(1186)) { - return MiniMenu.doAction(Action.DIALOGUE, 0, -1, 77725700) > 0; - } else if (Interfaces.isOpen(1189)) { - return MiniMenu.doAction(Action.DIALOGUE, 0, -1, 77922323) > 0; - } else if (Interfaces.isOpen(1191)) { - return MiniMenu.doAction(Action.DIALOGUE, 0, -1, 78053391) > 0; + return select(XApi.api()); + } + + public static List getOptions(GameAPI api) { + if (!api.isInterfaceOpen(1188)) { + return Collections.emptyList(); } - return false; + List options = new ArrayList<>(); + for (Component component : ComponentQuery.newQuery(api, 1188).id(6, 33, 35, 37, 39).results()) { + String text = api.getComponentText(component.interfaceId(), component.componentId()); + if (text != null && !text.isBlank()) { + options.add(text); + } + } + return options; } - /** - * Retrieves the options from the current dialogue interface if it is open. - * - * @return A list of options from the dialogue interface, or an empty list if the interface is not open. - */ - @NotNull public static List getOptions() { - if (Interfaces.isOpen(1188)) { - List options = new ArrayList<>(ComponentQuery.newQuery(1188).id(6,33,35,37,39).type(ComponentType.TEXT).results().stream().map(Component::getText).toList()); - options.removeIf(result -> result.getBytes(StandardCharsets.UTF_8).length < 1); - return options; - } - return Collections.emptyList(); + return getOptions(XApi.api()); } - /** - * Checks if the given string is present in the options of the current dialogue interface. - * - * @param string The string to check for in the options. - * @return True if the string is found in the options, false otherwise. - */ public static boolean hasOption(String string) { - return getOptions().stream().anyMatch(i -> i.contentEquals(string)); + return getOptions().stream().anyMatch(option -> option.contentEquals(string)); } - /** - * Interacts with the dialogue interface using the text of the option. - * - * @param optionText The text of the option to interact with. - * If the option is not found, it will return false. - * @return True if the interaction was successful, false otherwise. - */ - public static boolean interact(String optionText) { - if (Interfaces.isOpen(1188)) { - var result = ComponentQuery.newQuery(1188).type(ComponentType.TEXT).text(String::contentEquals, optionText).results().first(); - if (result != null) { - int slot = -1; - var options = getOptions(); - int size = options.size(); - for (int i = 0; i < size; i++) { - if (options.get(i).contains(optionText)) { - slot = i; - } - } - return interact(slot); + public static boolean interact(GameAPI api, String optionText) { + List options = getOptions(api); + for (int i = 0; i < options.size(); i++) { + if (options.get(i).contains(optionText)) { + return interact(api, i); } } return false; } - /** - * Interacts with the dialogue interface with the given index. The index is the position of the option in the dialogue, starting at 0. - * - * @param index The index of the option to interact with. - * If the index is -1, the first option will be selected. - * @return True if the interaction was successful, false otherwise. - */ + public static boolean interact(String optionText) { + return interact(XApi.api(), optionText); + } + public static boolean interact(int index) { - if (Interfaces.isOpen(1188)) { - if (index != -1) { - int[] opcode = new int[]{77856776, 77856781, 77856786, 77856791, 77856796}; - return MiniMenu.doAction(Action.DIALOGUE, 0, -1, opcode[index]) > 0; - } + return interact(XApi.api(), index); + } + + public static boolean interact(GameAPI api, int index) { + if (!api.isInterfaceOpen(1188) || index < 0 || index >= OPTION_HASHES.length) { + return false; } - return false; + api.queueAction(new GameAction(ActionTypes.DIALOGUE, 0, -1, OPTION_HASHES[index])); + return true; } - /** - * Retrieves the text from a component if the interface is open. - * - * @return The text from the component, or an empty string if the interface is not open. - */ - @Nullable public static String getText() { - if (Interfaces.isOpen(1184)) { - var result = ComponentQuery.newQuery(1184).id(10).results().first(); - if (result != null && result.getText() != null) { - return result.getText(); - } - } else if (Interfaces.isOpen(1189)) { - var result = ComponentQuery.newQuery(1189).id(3).results().first(); - if (result != null && result.getText() != null) { - return result.getText(); + return getText(XApi.api()); + } - } - } else if (Interfaces.isOpen(1186)) { - var result = ComponentQuery.newQuery(1186).id(3).results().first(); - if (result != null && result.getText() != null) { - return result.getText(); - } + public static String getText(GameAPI api) { + if (api.isInterfaceOpen(1184)) { + return text(api, 1184, 10); + } + if (api.isInterfaceOpen(1189)) { + return text(api, 1189, 3); + } + if (api.isInterfaceOpen(1186)) { + return text(api, 1186, 3); } return null; } - /** - * Retrieves the title from the current dialogue interface if it is open. - * - * @return The title of the dialogue interface, or null if the interface is not open or the title is not found. - */ - @Nullable public static String getTitle() { - Component result = null; - if (Interfaces.isOpen(1184)) { - result = ComponentQuery.newQuery(1184).type(ComponentType.TEXT).results().first(); - } else if (Interfaces.isOpen(1188)) { - result = ComponentQuery.newQuery(1188).type(ComponentType.TEXT).results().first(); - } else if (Interfaces.isOpen(1189)) { - result = ComponentQuery.newQuery(1189).type(ComponentType.TEXT).results().first(); - } else if (Interfaces.isOpen(1191)) { - result = ComponentQuery.newQuery(1191).type(ComponentType.TEXT).results().first(); + return getTitle(XApi.api()); + } + + public static String getTitle(GameAPI api) { + for (int interfaceId : DIALOG_INTERFACES) { + if (api.isInterfaceOpen(interfaceId)) { + Component component = ComponentQuery.newQuery(api, interfaceId).results().first(); + if (component != null) { + return text(api, component.interfaceId(), component.componentId()); + } + } } - return result != null ? result.getText() : null; + return null; + } + + private static String text(GameAPI api, int interfaceId, int componentId) { + return api.getComponentText(interfaceId, componentId); + } + + private static int resolveDefaultHash(GameAPI api) { + if (api.isInterfaceOpen(1184)) return 77594639; + if (api.isInterfaceOpen(1186)) return 77725700; + if (api.isInterfaceOpen(1189)) return 77922323; + if (api.isInterfaceOpen(1191)) return 78053391; + return 0; } } diff --git a/src/main/java/net/botwithus/xapi/game/hud/MiniMenuHelper.java b/src/main/java/net/botwithus/xapi/game/hud/MiniMenuHelper.java new file mode 100644 index 0000000..ff436eb --- /dev/null +++ b/src/main/java/net/botwithus/xapi/game/hud/MiniMenuHelper.java @@ -0,0 +1,32 @@ +package net.botwithus.xapi.game.hud; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.model.MiniMenuEntry; +import net.botwithus.xapi.XApi; + +import java.util.Collections; +import java.util.List; + +public final class MiniMenuHelper { + + private MiniMenuHelper() { + } + + public static List getEntries(GameAPI api) { + List entries = api.getMiniMenu(); + return entries == null ? Collections.emptyList() : entries; + } + + public static List getEntries() { + return getEntries(XApi.api()); + } + + public static boolean hasEntry(GameAPI api, String optionText) { + return getEntries(api).stream() + .anyMatch(entry -> entry.optionText() != null && entry.optionText().equalsIgnoreCase(optionText)); + } + + public static boolean hasEntry(String optionText) { + return hasEntry(XApi.api(), optionText); + } +} diff --git a/src/main/java/net/botwithus/xapi/game/inventory/Backpack.java b/src/main/java/net/botwithus/xapi/game/inventory/Backpack.java deleted file mode 100644 index d398c8e..0000000 --- a/src/main/java/net/botwithus/xapi/game/inventory/Backpack.java +++ /dev/null @@ -1,240 +0,0 @@ -package net.botwithus.xapi.game.inventory; - -import net.botwithus.rs3.inventories.Inventory; -import net.botwithus.rs3.inventories.InventoryManager; -import net.botwithus.rs3.item.InventoryItem; -import net.botwithus.rs3.interfaces.Component; -import net.botwithus.rs3.interfaces.Interfaces; -import net.botwithus.rs3.minimenu.Action; -import net.botwithus.rs3.minimenu.MiniMenu; - -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.function.BiFunction; -import java.util.regex.Pattern; - -public class Backpack { - - /** - * Retrieves the backpack inventory with ID 93. - * - * @return the backpack inventory - */ - public static Inventory getInventory() { - return InventoryManager.getInventory(93); - } - - /** - * Checks if the backpack is full. - * - * @return true if the backpack is full, false otherwise - */ - public static boolean isFull() { - Inventory backpack = getInventory(); - return backpack.getItems().stream().map(InventoryItem::getId).filter(i -> i != -1).count() == backpack.getDefinition().getCapacity(); - } - - /** - * Checks if the backpack is empty. - * - * @return true if the backpack is empty, false otherwise - */ - public static boolean isEmpty() { - Inventory backpack = getInventory(); - return backpack.getItems().stream().map(InventoryItem::getId).allMatch(id -> id == -1); - } - - /** - * Retrieves all items in the backpack. - * - * @return a list of all items in the backpack - */ - public static List getItems() { - return getInventory().getItems().stream().filter(i -> !i.getName().isEmpty()).toList(); - } - - /** - * Checks if the backpack contains any items with names matching the given predicate. - * - * @param spred the predicate to match item names - * @param names the names to check - * @return true if any item name matches the predicate, false otherwise - */ - public static boolean contains(BiFunction spred, String... names) { - if (names == null || names.length == 0) { - return false; - } - var sanitizedNames = Arrays.stream(names) - .filter(Objects::nonNull) - .toList(); - if (sanitizedNames.isEmpty()) { - return false; - } - Inventory backpack = getInventory(); - return backpack.getItems().stream() - .map(InventoryItem::getName) - .filter(Objects::nonNull) - .anyMatch(name -> sanitizedNames.stream().anyMatch(candidate -> Boolean.TRUE.equals(spred.apply(name, candidate)))); - } - - /** - * Checks if the backpack contains any items with the given names. - * - * @param names the names to check - * @return true if any item name matches, false otherwise - */ - public static boolean contains(String... names) { - return contains(String::contentEquals, names); - } - - /** - * Checks if the backpack contains any items with the given IDs. - * - * @param ids the IDs to check - * @return true if any item ID matches, false otherwise - */ - public static boolean contains(int... ids) { - Inventory backpack = getInventory(); - return backpack.getItems().stream().map(InventoryItem::getId).anyMatch(id -> Arrays.stream(ids).anyMatch(i -> i == id)); - } - - /** - * Checks if the backpack contains any items with names matching the given patterns. - * - * @param namePatterns the patterns to match item names - * @return true if any item name matches any of the patterns, false otherwise - */ - public static boolean contains(Pattern... namePatterns) { - Inventory backpack = getInventory(); - return backpack.getItems().stream().map(InventoryItem::getName).anyMatch(name -> Arrays.stream(namePatterns).anyMatch(p -> p.matcher(name).matches())); - } - - /** - * Retrieves the first item in the backpack with a name matching the given predicate. - * - * @param spred the predicate to match item names - * @param names the names to check - * @return the first matching item, or null if no match is found - */ - public static InventoryItem getItem(BiFunction spred, String... names) { - if (names == null || names.length == 0) { - return null; - } - var sanitizedNames = Arrays.stream(names) - .filter(Objects::nonNull) - .toList(); - if (sanitizedNames.isEmpty()) { - return null; - } - Inventory backpack = getInventory(); - return backpack.getItems().stream() - .filter(item -> item.getName() != null && sanitizedNames.stream().anyMatch(candidate -> Boolean.TRUE.equals(spred.apply(item.getName(), candidate)))) - .findFirst() - .orElse(null); - } - - /** - * Retrieves the first item in the backpack with the given name. - * - * @param names the names to check - * @return the first matching item, or null if no match is found - */ - public static InventoryItem getItem(String... names) { - return getItem(String::contentEquals, names); - } - - /** - * Retrieves the first item in the backpack with the given ID. - * - * @param ids the IDs to check - * @return the first matching item, or null if no match is found - */ - public static InventoryItem getItem(int... ids) { - Inventory backpack = getInventory(); - return backpack.getItems().stream().filter(item -> Arrays.stream(ids).anyMatch(i -> i == item.getId())).findFirst().orElse(null); - } - - /** - * Retrieves all items in the backpack that have the specified option. - * - * @param option the option to check for (e.g., "Eat", "Drink", "Use") - * @return a list of items that have the specified option - */ - public static List getItemsWithOption(String option) { - return getInventory().getItems().stream() - .filter(item -> item.getId() != -1 && !item.getName().isEmpty()) - .filter(item -> item.getOptions() != null && item.getOptions().contains(option)) - .toList(); - } - - /** - * Interacts with an item in the backpack using the specified option. - * - * @param itemName the name of the item to interact with - * @param option the option to use (e.g., "Eat", "Drink", "Use") - * @return true if the interaction was successful, false otherwise - */ - public static boolean interact(String itemName, String option) { - InventoryItem item = getItem(itemName); - return item != null && item.interact(option) > 0; - } - - /** - * Interacts with an item in the backpack by ID using the specified option. - * - * @param itemId the ID of the item to interact with - * @param option the option to use (e.g., "Eat", "Drink", "Use") - * @return true if the interaction was successful, false otherwise - */ - public static boolean interact(int itemId, String option) { - InventoryItem item = getItem(itemId); - return item != null && item.interact(option) > 0; - } - - /** - * Drags a component from one location to another using the v2 MiniMenu system. - * This is the v2 equivalent of the v1 Interfaces.dragComponents method. - * - * @param fromComponent the source component to drag from - * @param toComponent the destination component to drag to - * @return true if the drag operation was successful, false otherwise - */ - public static boolean dragComponent(Component fromComponent, Component toComponent) { - if (fromComponent == null || toComponent == null) { - return false; - } - - try { - // First, set the target to the source component - int fromInterfaceId = fromComponent.getRoot().getInterfaceId(); - int fromComponentId = fromComponent.getComponentId(); - int fromSubComponentId = fromComponent.getSubComponentId(); - - // Initiate drag from source component - int dragResult = MiniMenu.doAction(Action.COMPONENT_DRAG, - fromSubComponentId, - fromComponentId, - fromInterfaceId); - - if (dragResult <= 0) { - return false; - } - - // Complete drag to destination component - int toInterfaceId = toComponent.getRoot().getInterfaceId(); - int toComponentId = toComponent.getComponentId(); - int toSubComponentId = toComponent.getSubComponentId(); - - int dropResult = MiniMenu.doAction(Action.COMPONENT, - toSubComponentId, - toComponentId, - toInterfaceId); - - return dropResult > 0; - - } catch (Exception e) { - return false; - } - } -} \ No newline at end of file diff --git a/src/main/java/net/botwithus/xapi/game/inventory/Bank.java b/src/main/java/net/botwithus/xapi/game/inventory/Bank.java deleted file mode 100644 index 0548ee2..0000000 --- a/src/main/java/net/botwithus/xapi/game/inventory/Bank.java +++ /dev/null @@ -1,516 +0,0 @@ -package net.botwithus.xapi.game.inventory; - -import net.botwithus.rs3.interfaces.Component; -import net.botwithus.rs3.interfaces.Interfaces; -import net.botwithus.rs3.inventories.Inventory; -import net.botwithus.rs3.inventories.InventoryManager; -import net.botwithus.rs3.item.InventoryItem; -import net.botwithus.rs3.item.Item; -import net.botwithus.rs3.minimenu.Action; -import net.botwithus.rs3.minimenu.MiniMenu; -import net.botwithus.rs3.vars.VarDomain; -import net.botwithus.rs3.world.Distance; -import net.botwithus.util.Rand; -import net.botwithus.xapi.query.ComponentQuery; -import net.botwithus.xapi.query.InventoryItemQuery; -import net.botwithus.xapi.query.NpcQuery; -import net.botwithus.xapi.query.SceneObjectQuery; -import net.botwithus.xapi.query.result.ResultSet; -import net.botwithus.xapi.script.permissive.base.PermissiveScript; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; -import java.util.function.BiFunction; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -public class Bank { - private static final int PRESET_BROWSING_VARBIT_ID = 49662, SELECTED_OPTIONS_TAB_VARBIT_ID = 45191, WITHDRAW_TYPE_VARBIT_ID = 45189, WITHDRAW_X_VARP_ID = 111; - private static final Pattern BANK_NAME_PATTERN = Pattern.compile("^(?!.*deposit).*(bank|counter).*$", Pattern.CASE_INSENSITIVE); - private static final String LAST_PRESET_OPTION = "Load Last Preset from"; - public static int INVENTORY_ID = 95, INTERFACE_INDEX = 517, COMPONENT_INDEX = 202; - private static final Logger logger = LoggerFactory.getLogger(Bank.class); - - - private static int previousLoadedPreset = -1; - - /** - * Opens the nearest bank. - * - * @return {@code true} if the bank was successfully opened, {@code false} otherwise. - */ - public static boolean open() { - try { - logger.info("Attempting find bank obj"); - var obj = SceneObjectQuery.newQuery().name(BANK_NAME_PATTERN).option("Use") - .or(SceneObjectQuery.newQuery().name(BANK_NAME_PATTERN).option("Bank")) - .or(SceneObjectQuery.newQuery().name("Shantay chest")).results().nearest(); - - logger.info("Attempting find bank npc"); - var npc = NpcQuery.newQuery().option("Bank").results().nearest(); - logger.info("Bank opening initiated"); - var useObj = true; - - logger.info("Object is " + (obj != null ? "not null" : "null")); - logger.info("Npc is " + (npc != null ? "not null" : "null")); - - if (obj != null && npc != null) { - logger.info("Distance.to(obj): " + Distance.to(obj)); - logger.info("Distance.to(npc): " + Distance.to(npc)); - var objDist = Distance.to(obj); - var npcDist = Distance.to(npc); - if (!Double.isNaN(objDist) && !Double.isNaN(npcDist)) - useObj = Distance.to(obj) < Distance.to(npc); - logger.info("useObj: " + useObj); - } - if (obj != null && useObj) { - logger.info("Interacting via Object: " + obj.getName()); - var actions = obj.getOptions(); - logger.info("Available Options: " + actions); - if (!actions.isEmpty()) { - var action = actions.stream().filter(i -> i != null && !i.isEmpty()).findFirst(); - logger.info("action.isPresent(): " + action.isPresent()); - if (action.isPresent()) { - logger.info("Attempting to interact with bank object using action: " + action.get()); - var interactionResult = obj.interact(action.get()); - logger.info("Object interaction completed: " + interactionResult); - return interactionResult > 0; - } else { - logger.warn("No valid action found for bank object"); - return false; - } - } else { - logger.warn("No options available on bank object"); - return false; - } - } else if (npc != null) { - logger.info("Interacting via NPC"); - var interactionResult = npc.interact("Bank"); - logger.info("NPC interaction completed: " + interactionResult); - return interactionResult > 0; - } - logger.warn("No valid bank object or NPC found"); - return false; - } catch (Exception e) { - logger.error(e.getMessage(), e); - return false; - } - } - - /** - * Checks if the bank interface is currently open - * @return true if bank is open, false otherwise - */ - public static boolean isOpen() { - return Interfaces.isOpen(INTERFACE_INDEX); - } - - /** - * Closes the bank interface. - * - * @return true if the interface was closed, false otherwise - */ - public static boolean close() { - return MiniMenu.doAction(Action.COMPONENT, 1, -1, 33882430) > 0; - } - - /** - * Retrieves the backpack inventory with ID 93. - * - * @return the backpack inventory - */ - public static Inventory getInventory() { - return InventoryManager.getInventory(INVENTORY_ID); - } - - public static boolean loadLastPreset() { - var obj = SceneObjectQuery.newQuery() - .option(LAST_PRESET_OPTION).results().nearest(); - var npc = NpcQuery.newQuery().option(LAST_PRESET_OPTION).results().nearest(); - var useObj = true; - -// logger.debug("Object is " + (obj != null ? "not null" : "null")); -// logger.debug("Npc is " + (npc != null ? "not null" : "null")); - - if (obj != null && npc != null) { -// logger.debug("Distance.to(obj): " + Distance.to(obj)); -// logger.debug("Distance.to(npc): " + Distance.to(npc)); - var objDist = Distance.to(obj); - var npcDist = Distance.to(npc); - if (!Double.isNaN(objDist) && !Double.isNaN(npcDist)) - useObj = Distance.to(obj) < Distance.to(npc); -// logger.debug("useObj: " + useObj); - } - if (obj != null && useObj) { -// logger.debug("Interacting via Object: " + obj.getName()); - return obj.interact(LAST_PRESET_OPTION) > 0; - } else if (npc != null) { -// logger.debug("Interacting via Npc: " + npc.getName()); - return npc.interact(LAST_PRESET_OPTION) > 0; - } - return false; - } - - /** - * Gets all the items in the players bank - * - * @return returns an array containing all items in the bank. - */ - public static Item[] getItems() { - return InventoryItemQuery.newQuery(INVENTORY_ID).results().stream().filter(i -> i.getId() != -1).toArray(Item[]::new); - } - - /** - * Gets the count of a specific item in the bank. - * - * @param results the query results specifying the item to count. - * @return returns an integer representing the count of the item - */ - public static int count(ResultSet results) { - return results.stream().mapToInt(Item::getQuantity).sum(); - } - - /** - * Gets the first item matching the predicate. - * - * @param query the predicate specifying the item to count. - * @return returns the item, or null if not found. - */ - public static Item first(InventoryItemQuery query) { - return query.results().first(); - } - - /** - * Determines if the bank is empty - * - * @return returns true if empty, false if not. - */ - public static boolean isEmpty() { - return getItems().length == 0; - } - - public static boolean interact(int slot, int option) { - ResultSet results = InventoryItemQuery.newQuery(INVENTORY_ID).slot(slot).results(); - var item = results.first(); - if (item != null) { - logger.info("[Inventory#interact(slot, option)]: " + item.getId()); - ResultSet queryResults = ComponentQuery.newQuery(INTERFACE_INDEX).id(COMPONENT_INDEX).itemId(item.getId()).results(); - logger.info("[Inventory#interact(slot, option)]: QueryResults: " + queryResults.size()); - var result = queryResults.first(); - return result != null && result.interact(option) > 0; - } - return false; - } - - /** - * Determines if the bank contains an item. - * - * @param query the predicate specifying the item to count. - * @return returns the item, or null if not found. - */ - public static boolean contains(InventoryItemQuery query) { - return count(query.results()) > 0; - } - - public static boolean contains(String... itemNames) { - return !InventoryItemQuery.newQuery(INVENTORY_ID).name(itemNames).results().isEmpty(); - } - - public static boolean contains(Pattern itemNamePattern) { - return !InventoryItemQuery.newQuery(INVENTORY_ID).name(itemNamePattern).results().isEmpty(); - } - - public static int getCount(String... itemNames) { - return count(InventoryItemQuery.newQuery(INVENTORY_ID).name(itemNames).results()); - } - - public static int getCount(Pattern namePattern) { - return count(InventoryItemQuery.newQuery(INVENTORY_ID).name(namePattern).results()); - } - - /** - * Withdraws an item from the bank - * - * @param query the query specifying the item to withdraw. - * @param option the doAction option to execute on the item. - */ - public static boolean withdraw(InventoryItemQuery query, int option) { - setTransferOption(TransferOptionType.ALL); - var item = query.results().first(); - if (item != null) { - logger.info("Item: " + item.getName()); - } else { - logger.info("Item is null"); - } - return item != null && interact(item.getSlot(), option); - } - - /** - * Withdraws an item from the inventory. - * - * @param itemName The name of the item to withdraw. - * @param option The option to withdraw. - * @return True if the item was successfully withdrawn, false otherwise. - */ - public static boolean withdraw(String itemName, int option) { - if (itemName != null && !itemName.isEmpty()) { - return withdraw(InventoryItemQuery.newQuery(INVENTORY_ID).name(itemName), option); - } - return false; - } - - /** - * Withdraws an item from the inventory. - * - * @param itemId The ID of the item to withdraw. - * @param option The option of the item to withdraw. - * @return True if the item was successfully withdrawn, false otherwise. - */ - public static boolean withdraw(int itemId, int option) { - if (itemId >= 0) { - return withdraw(InventoryItemQuery.newQuery(INVENTORY_ID).id(itemId), option); - } - return false; - } - - /** - * Withdraws an item from the inventory. - * - * @param pattern The pattern of the item to withdraw. - * @param option The option of the item to withdraw. - * @return true if the item was successfully withdrawn, false otherwise. - */ - public static boolean withdraw(Pattern pattern, int option) { - if (pattern != null) { - return withdraw(InventoryItemQuery.newQuery(INVENTORY_ID).name(pattern), option); - } - return false; - } - - /** - * Withdraws all of a given item from the inventory. - * - * @param name The name of the item to withdraw. - * @return true if the item was successfully withdrawn, false otherwise. - */ - public static boolean withdrawAll(String name) { - return withdraw(InventoryItemQuery.newQuery(INVENTORY_ID).name(name), 1); - } - - public static boolean withdrawAll(int id) { - return withdraw(InventoryItemQuery.newQuery(INVENTORY_ID).id(id), 1); - } - - public static boolean withdrawAll(Pattern pattern) { - return withdraw(InventoryItemQuery.newQuery(INVENTORY_ID).name(pattern), 1); - } - - /** - * Deposits all items in the player's bank. - * - * @return true if the items were successfully deposited, false otherwise - */ - public static boolean depositAll() { - setTransferOption(TransferOptionType.ALL); - var comp = ComponentQuery.newQuery(INTERFACE_INDEX).option("Deposit carried items").results().first(); - return comp != null && comp.interact(1) > 0; - } - - /** - * Deposits all items in the player's bank. - * - * @return true if the items were successfully deposited, false otherwise - */ - public static boolean depositEquipment() { - Component component = ComponentQuery.newQuery(INTERFACE_INDEX).id(42).results().first(); - return component != null && component.interact(1) > 0; - } - - /** - * Deposits all items in the player's bank. - * - * @return true if the items were successfully deposited, false otherwise - */ - public static boolean depositBackpack() { - Component component = ComponentQuery.newQuery(INTERFACE_INDEX).id(39).results().first(); - return component != null && component.interact(1) > 0; - } - - - /** - * Attempts to deposit an item from the given {@link InventoryItemQuery}. - * - * @param script The script to use for depositing the item. - * @param query The query to use for finding the item to deposit. - * @param option The option to use when depositing the item. - * @return {@code true} if the item was successfully deposited, {@code false} otherwise. - */ - public static boolean deposit(PermissiveScript script, ComponentQuery query, int option) { - var item = query.results().first(); - return deposit(script, item, option); - } - - public static boolean depositAll(PermissiveScript script, ComponentQuery query) { - var item = query.results().first(); - return deposit(script, item, 1);//item.getOptions().contains("Deposit-All") ? 7 : 1); - } - - public static boolean deposit(PermissiveScript script, Component comp, int option) { - setTransferOption(TransferOptionType.ALL); - var val = comp != null && comp.interact(option) > 0; - if (val) script.delay(Rand.nextInt(1, 2)); - return val; - } - - public static boolean depositAll(PermissiveScript script, String... itemNames) { - return !InventoryItemQuery.newQuery(93).name(itemNames).results().stream().map(Item::getId).distinct().map( - i -> depositAll(script, ComponentQuery.newQuery(517).itemId(i)) - ).toList().contains(false); - } - - public static boolean depositAll(PermissiveScript script, int... itemIds) { - return !InventoryItemQuery.newQuery(93).id(itemIds).results().stream().map(Item::getId).distinct().map( - i -> depositAll(script, ComponentQuery.newQuery(517).itemId(i)) - ).toList().contains(false); - } - - public static boolean depositAll(PermissiveScript script, Pattern... patterns) { - return !InventoryItemQuery.newQuery(93).name(patterns).results().stream().map(Item::getId).distinct().map( - i -> depositAll(script, ComponentQuery.newQuery(517).itemId(i)) - ).toList().contains(false); - } - - public static boolean depositAllExcept(PermissiveScript script, String... itemNames) { - var names = itemNames == null ? new String[0] : itemNames; - var protectedNames = Arrays.stream(names) - .filter(name -> name != null && !name.isEmpty()) - .collect(Collectors.toSet()); - var protectedIds = Backpack.getItems().stream() - .filter(item -> item.getName() != null && protectedNames.contains(item.getName())) - .map(Item::getId) - .collect(Collectors.toSet()); - var items = ComponentQuery.newQuery(517).results().stream().filter( - component -> !protectedIds.contains(component.getItemId()) && (component.getOptions().contains("Deposit-All") || component.getOptions().contains("Deposit-1"))) - .map(Component::getItemId) - .collect(Collectors.toSet()); - return !items.stream().map(id -> depositAll(script, ComponentQuery.newQuery(517).itemId(id))).toList().contains(false); - } - - public static boolean depositAllExcept(PermissiveScript script, int... ids) { - var idSet = Arrays.stream(ids).boxed().collect(Collectors.toSet()); - var items = ComponentQuery.newQuery(517).results().stream().filter( - i -> !idSet.contains(i.getItemId()) && (i.getOptions().contains("Deposit-All") || i.getOptions().contains("Deposit-1"))) - .map(Component::getItemId) - .collect(Collectors.toSet()); - return !items.stream().map(i -> depositAll(script, ComponentQuery.newQuery(517).itemId(i))).toList().contains(false); - } - - public static boolean depositAllExcept(PermissiveScript script, Pattern... patterns) { - var idMap = Backpack.getItems().stream().filter(i -> i.getName() != null && Arrays.stream(patterns).map(p -> p.matcher(i.getName()).matches()).toList().contains(true)) - .collect(Collectors.toMap(Item::getId, Item::getName)); - var items = ComponentQuery.newQuery(517).results().stream().filter( - i -> !idMap.containsKey(i.getItemId()) && (i.getOptions().contains("Deposit-All") || i.getOptions().contains("Deposit-1"))) - .map(Component::getItemId) - .collect(Collectors.toSet()); - return !items.stream().map(i -> depositAll(script, ComponentQuery.newQuery(517).itemId(i))).toList().contains(false); - } - - /** - * Deposits an item into the inventory. - * - * @param itemId The ID of the item to deposit. - * @param option The option to use when depositing the item. - * @return True if the item was successfully deposited, false otherwise. - */ - public static boolean deposit(PermissiveScript script, int itemId, int option) { - return deposit(script, ComponentQuery.newQuery(517).itemId(itemId), option); - } - - /** - * Deposits an item into the inventory. - * - * @param name The name of the item to deposit. - * @param spred The spread function to use when searching for the item. - * @param option The option to use when depositing the item. - * @return True if the item was successfully deposited, false otherwise. - */ - public static boolean deposit(PermissiveScript script, String name, BiFunction spred, int option) { - return deposit(script, ComponentQuery.newQuery(517).itemName(name, spred), option); - } - - /** - * Deposits an amount of money into an account. - * - * @param name The name of the account to deposit into. - * @param option The amount of money to deposit. - * @return True if the deposit was successful, false otherwise. - */ - public static boolean deposit(PermissiveScript script, String name, int option) { - return deposit(script, name, String::contentEquals, option); - } - - /** - * Loads the given preset number. - * - * @param presetNumber the preset number to load - * @return true if the preset was successfully loaded, false otherwise - * @throws InterruptedException if the thread is interrupted while sleeping - */ - // TODO: Update to no longer use MiniMenu.doAction - public static boolean loadPreset(PermissiveScript script, int presetNumber) { - int presetBrowsingValue = VarDomain.getVarBitValue(PRESET_BROWSING_VARBIT_ID); - if ((presetNumber >= 10 && presetBrowsingValue < 1) || (presetNumber < 10 && presetBrowsingValue > 0)) { - MiniMenu.doAction(Action.COMPONENT, 1, 100, 33882231); - script.delay(Rand.nextInt(1, 2)); - } - var result = MiniMenu.doAction(Action.COMPONENT, 1, ((presetNumber - 1) % 9) + 1,33882231) > 0; - if (result) { - previousLoadedPreset = presetNumber; - } - return result; - } - - /** - * Gets the value of a varbit in the inventory. - * - * @param slot The inventory slot to check. - * @param varbitId The varbit id to check. - * @return The value of the varbit. - */ - public static int getVarbitValue(int slot, int varbitId) { - Inventory inventory = getInventory(); - if (inventory == null) { - return Integer.MIN_VALUE; - } - - return inventory.getVarbitValue(slot, varbitId); - } - - public static boolean setTransferOption(TransferOptionType transferoptionType) { - var depositOptionState = VarDomain.getVarBitValue(WITHDRAW_TYPE_VARBIT_ID); - return depositOptionState == transferoptionType.getVarbitStateValue() || MiniMenu.doAction(Action.COMPONENT, 1,-1, 33882215) > 0; - } - - public static int getPreviousLoadedPreset() { - return previousLoadedPreset; - } -} - -enum TransferOptionType { - ONE(2), - FIVE(3), - TEN(4), - ALL(7), - X(5); - - private int varbitStateValue; - - TransferOptionType(int varbitStateValue) { - this.varbitStateValue = varbitStateValue; - } - - public int getVarbitStateValue() { - return varbitStateValue; - } -} \ No newline at end of file diff --git a/src/main/java/net/botwithus/xapi/game/inventory/BwuBackpack.java b/src/main/java/net/botwithus/xapi/game/inventory/BwuBackpack.java new file mode 100644 index 0000000..ddd6ca4 --- /dev/null +++ b/src/main/java/net/botwithus/xapi/game/inventory/BwuBackpack.java @@ -0,0 +1,207 @@ +package net.botwithus.xapi.game.inventory; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.inventory.ActionTypes; +import com.botwithus.bot.api.inventory.Backpack; +import com.botwithus.bot.api.model.Component; +import com.botwithus.bot.api.model.GameAction; +import com.botwithus.bot.api.model.InventoryItem; +import com.botwithus.bot.api.query.ComponentFilter; +import net.botwithus.xapi.XApi; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.regex.Pattern; + +public final class BwuBackpack { + + public static final int INVENTORY_ID = Backpack.INVENTORY_ID; + public static final int INTERFACE_ID = Backpack.INTERFACE_ID; + public static final int COMPONENT_ID = Backpack.COMPONENT_ID; + + private BwuBackpack() { + } + + public static boolean isFull(GameAPI api) { + return container(api).isFull(); + } + + public static boolean isFull() { + return isFull(XApi.api()); + } + + public static boolean isEmpty(GameAPI api) { + return container(api).isEmpty(); + } + + public static boolean isEmpty() { + return isEmpty(XApi.api()); + } + + public static List getItems(GameAPI api) { + return container(api).getItems(); + } + + public static List getItems() { + return getItems(XApi.api()); + } + + public static boolean contains(GameAPI api, BiFunction matcher, String... names) { + return getItems(api).stream() + .map(item -> api.getItemType(item.itemId()).name()) + .filter(Objects::nonNull) + .anyMatch(name -> Arrays.stream(names).filter(Objects::nonNull) + .anyMatch(candidate -> Boolean.TRUE.equals(matcher.apply(name, candidate)))); + } + + public static boolean contains(BiFunction matcher, String... names) { + return contains(XApi.api(), matcher, names); + } + + public static boolean contains(GameAPI api, String... names) { + return contains(api, String::contentEquals, names); + } + + public static boolean contains(String... names) { + return contains(XApi.api(), names); + } + + public static boolean contains(GameAPI api, int... ids) { + return getItems(api).stream().anyMatch(item -> contains(ids, item.itemId())); + } + + public static boolean contains(int... ids) { + return contains(XApi.api(), ids); + } + + public static boolean contains(GameAPI api, Pattern... patterns) { + return getItems(api).stream() + .map(item -> api.getItemType(item.itemId()).name()) + .anyMatch(name -> matches(name, patterns)); + } + + public static boolean contains(Pattern... patterns) { + return contains(XApi.api(), patterns); + } + + public static InventoryItem getItem(GameAPI api, String... names) { + return getItems(api).stream() + .filter(item -> contains(names, api.getItemType(item.itemId()).name())) + .findFirst() + .orElse(null); + } + + public static InventoryItem getItem(String... names) { + return getItem(XApi.api(), names); + } + + public static InventoryItem getItem(GameAPI api, int... ids) { + return getItems(api).stream() + .filter(item -> contains(ids, item.itemId())) + .findFirst() + .orElse(null); + } + + public static InventoryItem getItem(int... ids) { + return getItem(XApi.api(), ids); + } + + public static List getItemsWithOption(GameAPI api, String option) { + return getItems(api).stream() + .filter(item -> api.getItemType(item.itemId()).inventoryOptions().stream() + .anyMatch(candidate -> candidate != null && candidate.equalsIgnoreCase(option))) + .toList(); + } + + public static List getItemsWithOption(String option) { + return getItemsWithOption(XApi.api(), option); + } + + public static boolean interact(GameAPI api, String itemName, String option) { + InventoryItem item = getItem(api, itemName); + return item != null && interact(api, item.itemId(), option); + } + + public static boolean interact(String itemName, String option) { + return interact(XApi.api(), itemName, option); + } + + public static boolean interact(GameAPI api, int itemId, String option) { + Component component = findComponentByItem(api, itemId); + if (component == null) { + return false; + } + List options = api.getComponentOptions(component.interfaceId(), component.componentId()); + for (int i = 0; i < options.size(); i++) { + if (option.equalsIgnoreCase(options.get(i))) { + api.queueAction(new GameAction(ActionTypes.COMPONENT, i + 1, component.subComponentId(), + component.interfaceId() << 16 | component.componentId())); + return true; + } + } + return false; + } + + public static boolean interact(int itemId, String option) { + return interact(XApi.api(), itemId, option); + } + + public static boolean dragComponent(GameAPI api, Component fromComponent, Component toComponent) { + if (fromComponent == null || toComponent == null) { + return false; + } + api.queueAction(new GameAction(ActionTypes.COMPONENT_DRAG, fromComponent.subComponentId(), fromComponent.componentId(), + fromComponent.interfaceId() << 16 | fromComponent.componentId())); + api.queueAction(new GameAction(ActionTypes.COMPONENT, 1, toComponent.subComponentId(), + toComponent.interfaceId() << 16 | toComponent.componentId())); + return true; + } + + public static boolean dragComponent(Component fromComponent, Component toComponent) { + return dragComponent(XApi.api(), fromComponent, toComponent); + } + + private static Backpack container(GameAPI api) { + return new Backpack(api); + } + + private static Component findComponentByItem(GameAPI api, int itemId) { + List components = api.queryComponents(ComponentFilter.builder() + .interfaceId(INTERFACE_ID) + .itemId(itemId) + .build()); + return components.isEmpty() ? null : components.getFirst(); + } + + private static boolean contains(int[] values, int actual) { + for (int value : values) { + if (value == actual) { + return true; + } + } + return false; + } + + private static boolean contains(String[] values, String actual) { + for (String value : values) { + if (value != null && value.equals(actual)) { + return true; + } + } + return false; + } + + private static boolean matches(String value, Pattern... patterns) { + if (value == null) { + return false; + } + for (Pattern pattern : patterns) { + if (pattern != null && pattern.matcher(value).matches()) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/botwithus/xapi/game/inventory/BwuBank.java b/src/main/java/net/botwithus/xapi/game/inventory/BwuBank.java new file mode 100644 index 0000000..e096905 --- /dev/null +++ b/src/main/java/net/botwithus/xapi/game/inventory/BwuBank.java @@ -0,0 +1,407 @@ +package net.botwithus.xapi.game.inventory; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.inventory.ActionTypes; +import com.botwithus.bot.api.inventory.Bank; +import com.botwithus.bot.api.inventory.Bank.TransferAmount; +import com.botwithus.bot.api.model.Component; +import com.botwithus.bot.api.model.GameAction; +import com.botwithus.bot.api.model.InventoryItem; +import net.botwithus.xapi.XApi; +import net.botwithus.xapi.query.ComponentQuery; +import net.botwithus.xapi.query.InventoryItemQuery; +import net.botwithus.xapi.query.NpcQuery; +import net.botwithus.xapi.query.SceneObjectQuery; +import net.botwithus.xapi.query.result.ResultSet; +import net.botwithus.xapi.script.permissive.base.PermissiveScript; + +import java.util.Arrays; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public final class BwuBank { + + public static final int INVENTORY_ID = Bank.INVENTORY_ID; + public static final int INTERFACE_INDEX = Bank.INTERFACE_ID; + public static final int COMPONENT_INDEX = Bank.BANK_COMPONENT; + + private static final Pattern BANK_NAME_PATTERN = Pattern.compile("^(?!.*deposit).*(bank|counter).*$", Pattern.CASE_INSENSITIVE); + private static final String LAST_PRESET_OPTION = "Load Last Preset from"; + + private BwuBank() { + } + + public static boolean open(GameAPI api) { + var obj = SceneObjectQuery.newQuery(api).name(BANK_NAME_PATTERN) + .option("Use").or(SceneObjectQuery.newQuery(api).name(BANK_NAME_PATTERN).option("Bank")).results().nearest(); + if (obj != null && (obj.interact("Bank") || obj.interact("Use"))) { + return true; + } + var npc = NpcQuery.newQuery(api).option("Bank").results().nearest(); + return npc != null && npc.interact("Bank"); + } + + public static boolean open() { + return open(XApi.api()); + } + + public static boolean isOpen(GameAPI api) { + return bank(api).isOpen(); + } + + public static boolean isOpen() { + return isOpen(XApi.api()); + } + + public static boolean close(GameAPI api) { + api.queueAction(new GameAction(ActionTypes.COMPONENT, 1, -1, INTERFACE_INDEX << 16 | 11)); + return true; + } + + public static boolean close() { + return close(XApi.api()); + } + + public static boolean loadLastPreset(GameAPI api) { + var obj = SceneObjectQuery.newQuery(api).option(LAST_PRESET_OPTION).results().nearest(); + if (obj != null && obj.interact(LAST_PRESET_OPTION)) { + return true; + } + var npc = NpcQuery.newQuery(api).option(LAST_PRESET_OPTION).results().nearest(); + return npc != null && npc.interact(LAST_PRESET_OPTION); + } + + public static boolean loadLastPreset() { + return loadLastPreset(XApi.api()); + } + + public static InventoryItem[] getItems(GameAPI api) { + return InventoryItemQuery.newQuery(api, INVENTORY_ID).results().stream() + .filter(item -> item.itemId() != -1) + .toArray(InventoryItem[]::new); + } + + public static InventoryItem[] getItems() { + return getItems(XApi.api()); + } + + public static int count(ResultSet results) { + return results.stream().mapToInt(InventoryItem::quantity).sum(); + } + + public static InventoryItem first(InventoryItemQuery query) { + return query.results().first(); + } + + public static boolean isEmpty() { + return getItems().length == 0; + } + + public static boolean interact(GameAPI api, int slot, int option) { + InventoryItem item = InventoryItemQuery.newQuery(api, INVENTORY_ID).slot(slot).results().first(); + return item != null && bank(api).withdraw(item.itemId(), mapOption(option)); + } + + public static boolean interact(int slot, int option) { + return interact(XApi.api(), slot, option); + } + + public static boolean contains(GameAPI api, InventoryItemQuery query) { + return count(query.results()) > 0; + } + + public static boolean contains(InventoryItemQuery query) { + return contains(XApi.api(), query); + } + + public static boolean contains(GameAPI api, String... itemNames) { + return !InventoryItemQuery.newQuery(api, INVENTORY_ID).name(itemNames).results().isEmpty(); + } + + public static boolean contains(String... itemNames) { + return contains(XApi.api(), itemNames); + } + + public static boolean contains(GameAPI api, Pattern itemNamePattern) { + return !InventoryItemQuery.newQuery(api, INVENTORY_ID).name(itemNamePattern).results().isEmpty(); + } + + public static boolean contains(Pattern itemNamePattern) { + return contains(XApi.api(), itemNamePattern); + } + + public static int getCount(GameAPI api, String... itemNames) { + return count(InventoryItemQuery.newQuery(api, INVENTORY_ID).name(itemNames).results()); + } + + public static int getCount(String... itemNames) { + return getCount(XApi.api(), itemNames); + } + + public static int getCount(GameAPI api, Pattern namePattern) { + return count(InventoryItemQuery.newQuery(api, INVENTORY_ID).name(namePattern).results()); + } + + public static int getCount(Pattern namePattern) { + return getCount(XApi.api(), namePattern); + } + + public static boolean withdraw(GameAPI api, InventoryItemQuery query, int option) { + InventoryItem item = query.results().first(); + return item != null && bank(api).withdraw(item.itemId(), mapOption(option)); + } + + public static boolean withdraw(InventoryItemQuery query, int option) { + return withdraw(XApi.api(), query, option); + } + + public static boolean withdraw(GameAPI api, String itemName, int option) { + return withdraw(api, InventoryItemQuery.newQuery(api, INVENTORY_ID).name(itemName), option); + } + + public static boolean withdraw(String itemName, int option) { + return withdraw(XApi.api(), itemName, option); + } + + public static boolean withdraw(GameAPI api, int itemId, int option) { + return withdraw(api, InventoryItemQuery.newQuery(api, INVENTORY_ID).id(itemId), option); + } + + public static boolean withdraw(int itemId, int option) { + return withdraw(XApi.api(), itemId, option); + } + + public static boolean withdraw(GameAPI api, Pattern pattern, int option) { + return withdraw(api, InventoryItemQuery.newQuery(api, INVENTORY_ID).name(pattern), option); + } + + public static boolean withdraw(Pattern pattern, int option) { + return withdraw(XApi.api(), pattern, option); + } + + public static boolean withdrawAll(GameAPI api, String name) { + return bank(api).withdrawAll(firstIdByName(api, name)); + } + + public static boolean withdrawAll(String name) { + return withdrawAll(XApi.api(), name); + } + + public static boolean withdrawAll(GameAPI api, int id) { + return bank(api).withdrawAll(id); + } + + public static boolean withdrawAll(int id) { + return withdrawAll(XApi.api(), id); + } + + public static boolean withdrawAll(GameAPI api, Pattern pattern) { + InventoryItem item = InventoryItemQuery.newQuery(api, INVENTORY_ID).name(pattern).results().first(); + return item != null && bank(api).withdrawAll(item.itemId()); + } + + public static boolean withdrawAll(Pattern pattern) { + return withdrawAll(XApi.api(), pattern); + } + + public static boolean depositAll(GameAPI api) { + return bank(api).depositAll(); + } + + public static boolean depositAll() { + return depositAll(XApi.api()); + } + + public static boolean depositEquipment(GameAPI api) { + return bank(api).depositEquipment(); + } + + public static boolean depositEquipment() { + return depositEquipment(XApi.api()); + } + + public static boolean depositBackpack(GameAPI api) { + return bank(api).depositAll(); + } + + public static boolean depositBackpack() { + return depositBackpack(XApi.api()); + } + + public static boolean deposit(GameAPI api, PermissiveScript script, ComponentQuery query, int option) { + Component item = query.results().first(); + return item != null && deposit(api, script, item, option); + } + + public static boolean deposit(PermissiveScript script, ComponentQuery query, int option) { + return deposit(XApi.api(), script, query, option); + } + + public static boolean depositAll(PermissiveScript script, ComponentQuery query) { + return deposit(script, query, 1); + } + + public static boolean deposit(GameAPI api, PermissiveScript script, Component component, int option) { + boolean queued = component != null && bank(api).deposit(component.itemId(), mapOption(option)); + if (queued) { + script.delay(1); + } + return queued; + } + + public static boolean deposit(PermissiveScript script, Component component, int option) { + return deposit(XApi.api(), script, component, option); + } + + public static boolean depositAll(GameAPI api, PermissiveScript script, String... itemNames) { + Set ids = Arrays.stream(itemNames) + .map(name -> firstIdByName(api, name)) + .filter(id -> id > -1) + .collect(Collectors.toSet()); + return ids.stream().allMatch(id -> bank(api).deposit(id, TransferAmount.ALL)); + } + + public static boolean depositAll(PermissiveScript script, String... itemNames) { + return depositAll(XApi.api(), script, itemNames); + } + + public static boolean depositAll(GameAPI api, PermissiveScript script, int... itemIds) { + return Arrays.stream(itemIds).allMatch(id -> bank(api).deposit(id, TransferAmount.ALL)); + } + + public static boolean depositAll(PermissiveScript script, int... itemIds) { + return depositAll(XApi.api(), script, itemIds); + } + + public static boolean depositAll(GameAPI api, PermissiveScript script, Pattern... patterns) { + Set ids = BwuBackpack.getItems(api).stream() + .filter(item -> { + String name = api.getItemType(item.itemId()).name(); + return Arrays.stream(patterns).anyMatch(pattern -> pattern.matcher(name).matches()); + }) + .map(InventoryItem::itemId) + .collect(Collectors.toSet()); + return ids.stream().allMatch(id -> bank(api).deposit(id, TransferAmount.ALL)); + } + + public static boolean depositAll(PermissiveScript script, Pattern... patterns) { + return depositAll(XApi.api(), script, patterns); + } + + public static boolean depositAllExcept(GameAPI api, PermissiveScript script, String... itemNames) { + Set protectedNames = Arrays.stream(itemNames).collect(Collectors.toSet()); + return BwuBackpack.getItems(api).stream() + .filter(item -> !protectedNames.contains(api.getItemType(item.itemId()).name())) + .map(InventoryItem::itemId) + .distinct() + .allMatch(id -> bank(api).deposit(id, TransferAmount.ALL)); + } + + public static boolean depositAllExcept(PermissiveScript script, String... itemNames) { + return depositAllExcept(XApi.api(), script, itemNames); + } + + public static boolean depositAllExcept(GameAPI api, PermissiveScript script, int... ids) { + Set protectedIds = Arrays.stream(ids).boxed().collect(Collectors.toSet()); + return BwuBackpack.getItems(api).stream() + .filter(item -> !protectedIds.contains(item.itemId())) + .map(InventoryItem::itemId) + .distinct() + .allMatch(id -> bank(api).deposit(id, TransferAmount.ALL)); + } + + public static boolean depositAllExcept(PermissiveScript script, int... ids) { + return depositAllExcept(XApi.api(), script, ids); + } + + public static boolean depositAllExcept(GameAPI api, PermissiveScript script, Pattern... patterns) { + return BwuBackpack.getItems(api).stream() + .filter(item -> { + String name = api.getItemType(item.itemId()).name(); + return Arrays.stream(patterns).noneMatch(pattern -> pattern.matcher(name).matches()); + }) + .map(InventoryItem::itemId) + .distinct() + .allMatch(id -> bank(api).deposit(id, TransferAmount.ALL)); + } + + public static boolean depositAllExcept(PermissiveScript script, Pattern... patterns) { + return depositAllExcept(XApi.api(), script, patterns); + } + + public static boolean deposit(GameAPI api, PermissiveScript script, int itemId, int option) { + return bank(api).deposit(itemId, mapOption(option)); + } + + public static boolean deposit(PermissiveScript script, int itemId, int option) { + return deposit(XApi.api(), script, itemId, option); + } + + public static boolean deposit(GameAPI api, PermissiveScript script, String name, BiFunction matcher, int option) { + Integer itemId = BwuBackpack.getItems(api).stream() + .filter(item -> Boolean.TRUE.equals(matcher.apply(api.getItemType(item.itemId()).name(), name))) + .map(InventoryItem::itemId) + .findFirst() + .orElse(-1); + return itemId > -1 && bank(api).deposit(itemId, mapOption(option)); + } + + public static boolean deposit(PermissiveScript script, String name, BiFunction matcher, int option) { + return deposit(XApi.api(), script, name, matcher, option); + } + + public static boolean deposit(PermissiveScript script, String name, int option) { + return deposit(script, name, String::contentEquals, option); + } + + public static boolean loadPreset(GameAPI api, PermissiveScript script, int presetNumber) { + return bank(api).withdrawPreset(presetNumber); + } + + public static boolean loadPreset(PermissiveScript script, int presetNumber) { + return loadPreset(XApi.api(), script, presetNumber); + } + + public static int getVarbitValue(GameAPI api, int slot, int varbitId) { + return api.getItemVarValue(INVENTORY_ID, slot, varbitId); + } + + public static int getVarbitValue(int slot, int varbitId) { + return getVarbitValue(XApi.api(), slot, varbitId); + } + + public static boolean setTransferOption(GameAPI api, TransferOptionType transferOptionType) { + return bank(api).setTransferMode(switch (transferOptionType) { + case ONE -> TransferAmount.ONE; + case FIVE -> TransferAmount.FIVE; + case TEN -> TransferAmount.TEN; + case ALL -> TransferAmount.ALL; + case X -> TransferAmount.CUSTOM; + }); + } + + public static boolean setTransferOption(TransferOptionType transferOptionType) { + return setTransferOption(XApi.api(), transferOptionType); + } + + private static Bank bank(GameAPI api) { + return new Bank(api); + } + + private static int firstIdByName(GameAPI api, String name) { + InventoryItem item = InventoryItemQuery.newQuery(api, INVENTORY_ID).name(name).results().first(); + return item == null ? -1 : item.itemId(); + } + + private static TransferAmount mapOption(int option) { + return switch (option) { + case 2 -> TransferAmount.ONE; + case 3 -> TransferAmount.FIVE; + case 4 -> TransferAmount.TEN; + case 5 -> TransferAmount.CUSTOM; + default -> TransferAmount.ALL; + }; + } +} diff --git a/src/main/java/net/botwithus/xapi/game/inventory/BwuEquipment.java b/src/main/java/net/botwithus/xapi/game/inventory/BwuEquipment.java new file mode 100644 index 0000000..d9c2394 --- /dev/null +++ b/src/main/java/net/botwithus/xapi/game/inventory/BwuEquipment.java @@ -0,0 +1,90 @@ +package net.botwithus.xapi.game.inventory; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.inventory.Equipment; +import com.botwithus.bot.api.inventory.Equipment.Slot; +import com.botwithus.bot.api.model.InventoryItem; +import net.botwithus.xapi.XApi; + +import java.util.List; +import java.util.regex.Pattern; + +public final class BwuEquipment { + + private BwuEquipment() { + } + + public static List getItems(GameAPI api) { + return equipment(api).getItems(); + } + + public static List getItems() { + return getItems(XApi.api()); + } + + public static boolean contains(GameAPI api, int itemId) { + return equipment(api).contains(itemId); + } + + public static boolean contains(int itemId) { + return contains(XApi.api(), itemId); + } + + public static InventoryItem getItem(GameAPI api, Slot slot) { + return equipment(api).getSlot(slot); + } + + public static InventoryItem getItem(Slot slot) { + return getItem(XApi.api(), slot); + } + + public static boolean interact(GameAPI api, Slot slot, int option) { + return equipment(api).interact(slot, option); + } + + public static boolean interact(Slot slot, int option) { + return interact(XApi.api(), slot, option); + } + + public static boolean interact(GameAPI api, Slot slot, String option) { + return equipment(api).interact(slot, option); + } + + public static boolean interact(Slot slot, String option) { + return interact(XApi.api(), slot, option); + } + + public static boolean interact(GameAPI api, int itemId, String option) { + return equipment(api).interact(itemId, option); + } + + public static boolean interact(int itemId, String option) { + return interact(XApi.api(), itemId, option); + } + + public static boolean equip(GameAPI api, int itemId) { + return BwuBackpack.interact(api, itemId, "Wear") + || BwuBackpack.interact(api, itemId, "Wield") + || BwuBackpack.interact(api, itemId, "Equip"); + } + + public static boolean equip(int itemId) { + return equip(XApi.api(), itemId); + } + + public static boolean equip(GameAPI api, Pattern pattern) { + InventoryItem item = BwuBackpack.getItems(api).stream() + .filter(candidate -> pattern.matcher(api.getItemType(candidate.itemId()).name()).matches()) + .findFirst() + .orElse(null); + return item != null && equip(api, item.itemId()); + } + + public static boolean equip(Pattern pattern) { + return equip(XApi.api(), pattern); + } + + private static Equipment equipment(GameAPI api) { + return new Equipment(api); + } +} diff --git a/src/main/java/net/botwithus/xapi/game/inventory/Equipment.java b/src/main/java/net/botwithus/xapi/game/inventory/Equipment.java deleted file mode 100644 index fd4d82a..0000000 --- a/src/main/java/net/botwithus/xapi/game/inventory/Equipment.java +++ /dev/null @@ -1,160 +0,0 @@ -package net.botwithus.xapi.game.inventory; - -import net.botwithus.rs3.inventories.Inventory; -import net.botwithus.rs3.inventories.InventoryManager; -import net.botwithus.rs3.item.InventoryItem; -import net.botwithus.rs3.vars.VarDomain; -import net.botwithus.xapi.util.Regex; - -import java.util.Arrays; -import java.util.regex.Pattern; - -public final class Equipment { - - /** - * Retrieves the equipment inventory with ID 94. - * - * @return the equipment inventory - */ - public static Inventory getInventory() { - return InventoryManager.getInventory(94); - } - - /** - * Gets the item in the specified slot. - * - * @param slot The slot to get the item from. - * @return The item in the slot, or null if there is no item in the slot. - */ - public static InventoryItem getItemIn(Slot slot) { - return getInventory().getItem(slot.getIndex()); - } - - /** - * Checks if the given item name is present in the equipment. - * - * @param name The name of the item to check for. - * @return true if the item is present in the equipment, false otherwise. - */ - public static boolean contains(String name) { - return getInventory().getItems().stream() - .filter(item -> item.getId() != -1 && item.getName() != null) - .anyMatch(item -> item.getName().equals(name)); - } - - /** - * Checks if the equipment contains an item with a name matching the given pattern. - * - * @param pattern The pattern to match the item name against. - * @return True if an item with a matching name is present in the equipment, false otherwise. - */ - public static boolean contains(Pattern pattern) { - return getInventory().getItems().stream() - .filter(item -> item.getId() != -1 && item.getName() != null) - .anyMatch(item -> pattern.matcher(item.getName()).matches()); - } - - /** - * Interacts with the item in the given slot. - * - * @param slot The slot to interact with. - * @param option The option to interact with. - * @return True if the interaction was successful, false otherwise. - */ - public static boolean interact(Slot slot, String option) { - InventoryItem item = getItemIn(slot); - return item != null && item.interact(option) > 0; - } - - /** - * Interacts with the item in the given slot using a pattern-matched option. - * - * @param slot The slot to interact with. - * @param option The pattern to match the option against. - * @return True if the interaction was successful, false otherwise. - */ - public static boolean interact(Slot slot, Pattern option) { - InventoryItem item = getItemIn(slot); - if (item != null && item.getOptions() != null) { - for (String availableOption : item.getOptions()) { - if (availableOption != null && option.matcher(availableOption).matches()) { - return item.interact(availableOption) > 0; - } - } - } - return false; - } - - /** - * Removes an item from the specified slot. - * - * @param slot The slot to unequip from. - * @return true if the item was successfully unequipped, false otherwise. - */ - public static boolean unequip(Slot slot) { - return interact(slot, "Remove"); - } - - /** - * Equips an item in the specified slot. - * - * @param slot The slot to equip to. - * @return true if the item was successfully equipped, false otherwise. - */ - public static boolean equip(Slot slot) { - return interact(slot, Regex.getPatternForExactStrings("Wear", "Wield", "Equip")); - } - - /** - * Gets the value of a varbit for an item in the specified slot. - * - * @param slot The equipment slot to check. - * @param varbitId The varbit ID to check. - * @return The value of the varbit, or -1 if the varbit is not present or item is null. - */ - public static int getVarbitValue(int slot, int varbitId) { - Inventory inventory = getInventory(); - if (inventory == null) { - return -1; - } - return inventory.getVarbitValue(slot, varbitId); - } - - /** - * Represents a slot in the player's equipment. - */ - public enum Slot { - HEAD(0, 24431), - CAPE(1, 24432), - NECK(2, 24433), - WEAPON(3, 24434), - BODY(4, 24436), - SHIELD(5, 24437), - LEGS(7, 24438), - HANDS(9, 24439), - FEET(10, 24440), - RING(12, 24435), - AMMUNITION(13, 24441), - AURA(14, 24442), - POCKET(17, 24443); - - private final int index; - - Slot(int index, int textureId) { - this.index = index; - } - - public static Slot resolve(int index) { - return Arrays.stream(values()).filter((slot) -> slot.index == index).findAny().orElse(null); - } - - public final int getIndex() { - return this.index; - } - - @Override - public String toString() { - return name().substring(0, 1).toUpperCase() + name().substring(1).toLowerCase(); - } - } -} \ No newline at end of file diff --git a/src/main/java/net/botwithus/xapi/game/inventory/TransferOptionType.java b/src/main/java/net/botwithus/xapi/game/inventory/TransferOptionType.java new file mode 100644 index 0000000..5e9d9b5 --- /dev/null +++ b/src/main/java/net/botwithus/xapi/game/inventory/TransferOptionType.java @@ -0,0 +1,9 @@ +package net.botwithus.xapi.game.inventory; + +public enum TransferOptionType { + ONE, + FIVE, + TEN, + ALL, + X +} diff --git a/src/main/java/net/botwithus/xapi/game/traversal/LodestoneNetwork.java b/src/main/java/net/botwithus/xapi/game/traversal/LodestoneNetwork.java index 757439a..9347e61 100644 --- a/src/main/java/net/botwithus/xapi/game/traversal/LodestoneNetwork.java +++ b/src/main/java/net/botwithus/xapi/game/traversal/LodestoneNetwork.java @@ -1,31 +1,29 @@ package net.botwithus.xapi.game.traversal; -import net.botwithus.rs3.entities.LocalPlayer; -import net.botwithus.rs3.interfaces.Interfaces; -import net.botwithus.rs3.vars.VarDomain; -import net.botwithus.util.Rand; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.inventory.ActionTypes; +import com.botwithus.bot.api.model.Component; +import com.botwithus.bot.api.model.GameAction; +import net.botwithus.xapi.XApi; import net.botwithus.xapi.game.traversal.enums.LodestoneType; import net.botwithus.xapi.query.ComponentQuery; import net.botwithus.xapi.script.permissive.base.PermissiveScript; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -public class LodestoneNetwork { - private static final Logger logger = LoggerFactory.getLogger(LodestoneNetwork.class); +public final class LodestoneNetwork { + + private LodestoneNetwork() { + } + + public static boolean isOpen(GameAPI api) { + return api.isInterfaceOpen(1092); + } public static boolean isOpen() { - var open = Interfaces.isOpen(1092); - if (logger.isDebugEnabled()) { - logger.debug("Lodestone network interface {}", open ? "is open" : "is not open"); - } - return open; + return isOpen(XApi.api()); } - public static boolean isAvailable(LodestoneType type) { - int result = VarDomain.getVarBitValue(type.getVarbitId()); - if (logger.isDebugEnabled()) { - logger.debug("Availability check for {} returned {} (varbit={})", type, result, type.getVarbitId()); - } + public static boolean isAvailable(GameAPI api, LodestoneType type) { + int result = api.getVarbit(type.getVarbitId()); return switch (type) { case LUNAR_ISLE -> result >= 100; case BANDIT_CAMP -> result >= 15; @@ -33,124 +31,53 @@ public static boolean isAvailable(LodestoneType type) { }; } - /** - * Opens the Lodestone Network interface. - * - * @return {@code true} if the interface was opened, {@code false} otherwise. - */ - public static boolean open() { - logger.info("Attempting to open the Lodestone network interface"); - var result = ComponentQuery.newQuery(1465).option("Lodestone network").results().first(); - if (result == null) { - logger.warn("Unable to locate the Lodestone network component"); - return false; - } - - var interactionResult = result.interact("Lodestone network"); - if (interactionResult > 0) { - logger.info("Successfully opened the Lodestone network interface"); - return true; - } - - logger.warn("Failed to open the Lodestone network interface (interaction result: {})", interactionResult); - return false; + public static boolean isAvailable(LodestoneType type) { + return isAvailable(XApi.api(), type); } - /** - * Teleports the player using the specified Lodestone. - * - * @param script the executing script instance controlling delays - * @param type the Lodestone to teleport to - * @return {@code true} if the teleport was initiated, {@code false} otherwise - */ - public static boolean teleport(PermissiveScript script, LodestoneType type) { - logger.info("Attempting to teleport using {}", type); - var player = LocalPlayer.self(); - if (player == null) { - logger.warn("Cannot teleport via {} because the local player is null", type); + public static boolean open(GameAPI api) { + Component component = ComponentQuery.newQuery(api, 1465).option("Lodestone network").results().first(); + if (component == null) { return false; } + api.queueAction(new GameAction(ActionTypes.COMPONENT, 1, + component.subComponentId(), component.interfaceId() << 16 | component.componentId())); + return true; + } - script.delay(Rand.nextInt(4500, 6500)); + public static boolean open() { + return open(XApi.api()); + } - if (!isOpen()) { - logger.debug("Lodestone network interface is closed. Opening before teleporting via {}", type); - if (!open()) { - logger.warn("Failed to open the Lodestone network interface for {}", type); - return false; - } - if (!isOpen()) { - int waitTicks = Rand.nextInt(18, 26); - logger.debug("Waiting up to {} ticks for the Lodestone network interface to open for {}", waitTicks, type); - script.delayUntil(LodestoneNetwork::isOpen, waitTicks); - if (!isOpen()) { - return false; - } - } + public static boolean teleport(GameAPI api, PermissiveScript script, LodestoneType type) { + if (!isOpen(api) && !open(api)) { + return false; } - - logger.debug("Lodestone network interface open; locating component for {}", type); - int interfaceId = LodestoneType.getInterfaceId(); - var component = Interfaces.getComponent(interfaceId, type.getComponentId()); + Component component = ComponentQuery.newQuery(api, LodestoneType.getInterfaceId()).id(type.getComponentId()).results().first(); if (component == null) { - logger.debug( - "Teleport component for {} not yet available (interfaceId={}, componentId={}); delaying before retrying", - type, - interfaceId, - type.getComponentId() - ); - script.delay(Rand.nextInt(8, 12)); return false; } + api.queueAction(new GameAction(ActionTypes.COMPONENT, 1, + component.subComponentId(), component.interfaceId() << 16 | component.componentId())); + script.delay(20); + return true; + } - int interactionResult = component.interact("Teleport"); - var teleportAction = type.getTeleportAction(); - if (interactionResult <= 0 && teleportAction != null) { - interactionResult = component.interact(teleportAction); - } - if (interactionResult <= 0) { - interactionResult = component.interact(); - } - if (interactionResult <= 0) { - logger.warn("Failed to interact with {} (interaction result: {}). Retrying after short delay.", type, interactionResult); - script.delay(Rand.nextInt(10, 16)); - return false; - } + public static boolean teleport(PermissiveScript script, LodestoneType type) { + return teleport(XApi.api(), script, type); + } - int wax = VarDomain.getVarBitValue(28623); - int quick = VarDomain.getVarBitValue(28622); - if (logger.isDebugEnabled()) { - logger.debug("Teleport interaction succeeded for {} (quick={}, wax={})", type, quick, wax); - } - if (quick == 1 && wax > 0) { - script.delay(Rand.nextInt(4500, 6500)); - } else { - script.delay(Rand.nextInt(12000, 14000)); + public static boolean teleportToPreviousDestination(GameAPI api) { + Component component = ComponentQuery.newQuery(api, 1465).option("Previous Destination").results().first(); + if (component == null) { + return false; } - logger.info("Teleport initiated for {}", type); + api.queueAction(new GameAction(ActionTypes.COMPONENT, 1, + component.subComponentId(), component.interfaceId() << 16 | component.componentId())); return true; } - /** - * Teleports the player to their previous destination. - * - * @return true if the player was successfully teleported, false otherwise - */ public static boolean teleportToPreviousDestination() { - logger.info("Attempting to teleport to the previous destination"); - var result = ComponentQuery.newQuery(1465).option("Previous Destination").results().first(); - if (result == null) { - logger.warn("Unable to locate the Previous Destination component"); - return false; - } - - var interactionResult = result.interact("Previous Destination"); - if (interactionResult > 0) { - logger.info("Teleport to the previous destination initiated"); - return true; - } - - logger.warn("Failed to teleport to the previous destination (interaction result: {})", interactionResult); - return false; + return teleportToPreviousDestination(XApi.api()); } } diff --git a/src/main/java/net/botwithus/xapi/game/traversal/MagicCarpetNetwork.java b/src/main/java/net/botwithus/xapi/game/traversal/MagicCarpetNetwork.java index b75db0b..760bf0c 100644 --- a/src/main/java/net/botwithus/xapi/game/traversal/MagicCarpetNetwork.java +++ b/src/main/java/net/botwithus/xapi/game/traversal/MagicCarpetNetwork.java @@ -1,26 +1,28 @@ package net.botwithus.xapi.game.traversal; -import net.botwithus.rs3.interfaces.InterfaceManager; -import net.botwithus.rs3.interfaces.Interfaces; +import com.botwithus.bot.api.GameAPI; +import net.botwithus.xapi.XApi; import net.botwithus.xapi.query.NpcQuery; -public class MagicCarpetNetwork { - /** - * Checks if the interface with the given ID is open. - * - * @return true if the interface is open, false otherwise. - */ +public final class MagicCarpetNetwork { + + private MagicCarpetNetwork() { + } + + public static boolean isOpen(GameAPI api) { + return api.isInterfaceOpen(1928); + } + public static boolean isOpen() { - return Interfaces.isOpen(1928); + return isOpen(XApi.api()); + } + + public static boolean open(GameAPI api) { + var npc = NpcQuery.newQuery(api).name("Rug merchant").option("Travel").results().nearest(); + return npc != null && npc.interact("Travel"); } - /** - * Opens the rug merchant's shop. - * - * @return {@code true} if the shop was successfully opened, {@code false} otherwise. - */ public static boolean open() { - var npc = NpcQuery.newQuery().name("Rug merchant").option("Travel").results().nearest(); - return npc != null && npc.interact("Travel") > 0; + return open(XApi.api()); } } diff --git a/src/main/java/net/botwithus/xapi/game/traversal/Traverse.java b/src/main/java/net/botwithus/xapi/game/traversal/Traverse.java index 814ed05..ddb499c 100644 --- a/src/main/java/net/botwithus/xapi/game/traversal/Traverse.java +++ b/src/main/java/net/botwithus/xapi/game/traversal/Traverse.java @@ -1,109 +1,74 @@ package net.botwithus.xapi.game.traversal; -import net.botwithus.rs3.entities.LocalPlayer; -import net.botwithus.rs3.minimenu.Action; -import net.botwithus.rs3.minimenu.MiniMenu; -import net.botwithus.rs3.world.Coordinate; -import net.botwithus.rs3.world.Distance; -import net.botwithus.util.Rand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.inventory.ActionTypes; +import com.botwithus.bot.api.model.GameAction; +import com.botwithus.bot.api.model.LocalPlayer; +import net.botwithus.xapi.XApi; -public class Traverse { - private final static Logger logger = LoggerFactory.getLogger(Traverse.class); +import java.util.concurrent.ThreadLocalRandom; + +public final class Traverse { private static final int MAX_LOCAL_DISTANCE = 80; private static final int MAX_STEP_SIZE = 16; private static final int MIN_STEP_SIZE = 10; - /** - * Walks to a coordinate, automatically choosing minimap usage and step size. - * If the distance to the destination is less than 24, does not use minimap; otherwise, uses minimap. - * Step size is randomized between 10 and 16 (inclusive). - * @param destinationCoord The destination coordinate - * @return true if walking was initiated successfully - */ - public static boolean to(Coordinate destinationCoord) { - if (destinationCoord == null) { - logger.warn("ERROR: Coordinate is null"); - return false; - } - var distance = Distance.to(destinationCoord); - var useMinimap = distance >= Rand.nextInt(22, 28); - var stepSize = Rand.nextInt(MIN_STEP_SIZE, MAX_STEP_SIZE + 1); - return bresenhamTo(destinationCoord, useMinimap, stepSize); + private Traverse() { } - - /** - * Walks to a coordinate using Bresenham line algorithm for pathfinding - * @param destinationCoord The destination coordinate - * @param minimap Whether to use minimap for walking (currently ignored, uses MiniMenu) - * @param stepSize Maximum step size for each movement - * @return true if walking was initiated successfully - */ - public static boolean bresenhamTo(Coordinate destinationCoord, boolean minimap, int stepSize) { - LocalPlayer player = LocalPlayer.self(); - if (player == null) { - logger.warn("[Traverse#bresenham] Player is null"); - return false; - } - Coordinate currentCoordinate = player.getCoordinate(); - if (currentCoordinate == null) { - logger.warn("[Traverse#bresenham] Current coordinate is null"); + public static boolean to(GameAPI api, int tileX, int tileY, int plane) { + LocalPlayer player = api.getLocalPlayer(); + if (player == null || player.plane() != plane) { return false; } + int distance = Math.max(Math.abs(tileX - player.tileX()), Math.abs(tileY - player.tileY())); + boolean useMinimap = distance >= ThreadLocalRandom.current().nextInt(22, 28); + int stepSize = ThreadLocalRandom.current().nextInt(MIN_STEP_SIZE, MAX_STEP_SIZE + 1); + return bresenhamTo(api, tileX, tileY, plane, useMinimap, stepSize); + } - int dx = destinationCoord.x() - currentCoordinate.x(); - int dy = destinationCoord.y() - currentCoordinate.y(); - int distance = (int)Math.hypot(dx, dy); - - if (distance > stepSize) { - int stepX = destinationCoord.x() + dx * stepSize / distance; - int stepY = destinationCoord.y() + dy * stepSize / distance; - return walkTo(new Coordinate(stepX, stepY, destinationCoord.z()), minimap); - } else { - return walkTo(destinationCoord, minimap); - } + public static boolean to(int tileX, int tileY, int plane) { + return to(XApi.api(), tileX, tileY, plane); } - /** - * Walks to a coordinate using MiniMenu - * @param destinationCoord The destination coordinate - * @param minimap Whether to use minimap for walking (currently ignored, uses MiniMenu) - * @return true if walking was initiated successfully - */ - public static boolean walkTo(Coordinate destinationCoord, boolean minimap) { - if (destinationCoord == null) { - logger.warn("ERROR: Coordinate is null"); + public static boolean bresenhamTo(GameAPI api, int tileX, int tileY, int plane, boolean minimap, int stepSize) { + LocalPlayer player = api.getLocalPlayer(); + if (player == null || player.plane() != plane) { return false; } + int dx = tileX - player.tileX(); + int dy = tileY - player.tileY(); + int distance = (int) Math.hypot(dx, dy); + if (distance > stepSize) { + int stepX = player.tileX() + dx * stepSize / distance; + int stepY = player.tileY() + dy * stepSize / distance; + return walkTo(api, stepX, stepY, plane, minimap); + } + return walkTo(api, tileX, tileY, plane, minimap); + } - try { - logger.info("Attempting to walk to " + destinationCoord.x() + ", " + destinationCoord.y()); - - if (Distance.to(destinationCoord) < 2) { - logger.info("Already close to target location, skipping walk"); - return true; - } - - if (Distance.to(destinationCoord) > MAX_LOCAL_DISTANCE) { - logger.info("Target location is too far away, using Bresenham pathfinding"); - return bresenhamTo(destinationCoord, minimap, Rand.nextInt(MIN_STEP_SIZE, MAX_STEP_SIZE)); - } - - int result = MiniMenu.doAction(Action.WALK, minimap ? 1 : 0, destinationCoord.x(), destinationCoord.y()); + public static boolean bresenhamTo(int tileX, int tileY, int plane, boolean minimap, int stepSize) { + return bresenhamTo(XApi.api(), tileX, tileY, plane, minimap, stepSize); + } - if (result > 0) { - logger.info("Successfully initiated walk to " + destinationCoord.x() + ", " + destinationCoord.y()); - return true; - } else { - logger.warn("Failed to walk to " + destinationCoord.x() + ", " + destinationCoord.y() + " - result: " + result); - return false; - } - } catch (Exception e) { - logger.trace("Exception while walking to " + destinationCoord.x() + ", " + destinationCoord.y() + ": " + e.getMessage(), e); + public static boolean walkTo(GameAPI api, int tileX, int tileY, int plane, boolean minimap) { + LocalPlayer player = api.getLocalPlayer(); + if (player == null || player.plane() != plane) { return false; } + int distance = Math.max(Math.abs(tileX - player.tileX()), Math.abs(tileY - player.tileY())); + if (distance < 2) { + return true; + } + if (distance > MAX_LOCAL_DISTANCE) { + return bresenhamTo(api, tileX, tileY, plane, minimap, ThreadLocalRandom.current().nextInt(MIN_STEP_SIZE, MAX_STEP_SIZE)); + } + api.queueAction(new GameAction(ActionTypes.WALK, minimap ? 1 : 0, tileX, tileY)); + return true; + } + + public static boolean walkTo(int tileX, int tileY, int plane, boolean minimap) { + return walkTo(XApi.api(), tileX, tileY, plane, minimap); } } diff --git a/src/main/java/net/botwithus/xapi/game/traversal/enums/MagicCarpetType.java b/src/main/java/net/botwithus/xapi/game/traversal/enums/MagicCarpetType.java index 81717e1..8172f2c 100644 --- a/src/main/java/net/botwithus/xapi/game/traversal/enums/MagicCarpetType.java +++ b/src/main/java/net/botwithus/xapi/game/traversal/enums/MagicCarpetType.java @@ -1,7 +1,9 @@ package net.botwithus.xapi.game.traversal.enums; -import net.botwithus.rs3.minimenu.Action; -import net.botwithus.rs3.minimenu.MiniMenu; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.inventory.ActionTypes; +import com.botwithus.bot.api.model.GameAction; +import net.botwithus.xapi.XApi; import net.botwithus.xapi.game.traversal.MagicCarpetNetwork; public enum MagicCarpetType { @@ -26,14 +28,15 @@ public int getId() { return interfaceIndex << 16 | componentIndex; } - //TODO: Update to no longer use MiniMenu.doAction - public boolean teleport() { - if (!MagicCarpetNetwork.isOpen()) { - MagicCarpetNetwork.open(); - } else { - return MiniMenu.doAction(Action.COMPONENT, 1, -1, getId()) > 0; + public boolean teleport(GameAPI api) { + if (!MagicCarpetNetwork.isOpen(api) && !MagicCarpetNetwork.open(api)) { + return false; } - return false; + api.queueAction(new GameAction(ActionTypes.COMPONENT, 1, -1, getId())); + return true; } -} + public boolean teleport() { + return teleport(XApi.api()); + } +} diff --git a/src/main/java/net/botwithus/xapi/query/ComponentQuery.java b/src/main/java/net/botwithus/xapi/query/ComponentQuery.java index bd93786..00435c8 100644 --- a/src/main/java/net/botwithus/xapi/query/ComponentQuery.java +++ b/src/main/java/net/botwithus/xapi/query/ComponentQuery.java @@ -1,297 +1,140 @@ package net.botwithus.xapi.query; -import net.botwithus.rs3.cache.assets.ConfigManager; -import net.botwithus.rs3.interfaces.Component; -import net.botwithus.rs3.interfaces.ComponentType; -import net.botwithus.rs3.interfaces.InterfaceManager; -import net.botwithus.rs3.interfaces.Interfaces; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.model.Component; +import com.botwithus.bot.api.query.ComponentFilter; +import net.botwithus.xapi.XApi; import net.botwithus.xapi.query.base.Query; import net.botwithus.xapi.query.result.ResultSet; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Iterator; -import java.util.Objects; +import java.util.List; import java.util.function.BiFunction; import java.util.function.Predicate; public class ComponentQuery implements Query> { - protected Predicate root; - private int[] ids; + private final GameAPI api; + private final int[] interfaceIds; + private Predicate filter; - /** - * Constructs a new ComponentQuery with the specified IDs. - * - * @param ids the IDs to query - */ - public ComponentQuery(int... ids) { - this.ids = ids; - root = t -> Arrays.stream(ids).anyMatch(i -> i == t.getRoot().getInterfaceId()); + public ComponentQuery(GameAPI api, int... interfaceIds) { + this.api = api; + this.interfaceIds = interfaceIds; + this.filter = component -> interfaceIds.length == 0 || contains(interfaceIds, component.interfaceId()); } - /** - * Creates a new ComponentQuery with the specified IDs. - * - * @param ids the IDs to query - * @return a new ComponentQuery instance - */ - public static ComponentQuery newQuery(int... ids) { - return new ComponentQuery(ids); + public static ComponentQuery newQuery(int... interfaceIds) { + return new ComponentQuery(XApi.api(), interfaceIds); } - /** - * Filters components by type. - * - * @param type the types to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery type(ComponentType... type) { - this.root = this.root.and(t -> Arrays.stream(type).anyMatch(i -> t.getType() == i)); - return this; + public static ComponentQuery newQuery(GameAPI api, int... interfaceIds) { + return new ComponentQuery(api, interfaceIds); } - /** - * Filters components by ID. - * - * @param ids the IDs to filter by - * @return the updated ComponentQuery - */ public ComponentQuery id(int... ids) { - this.root = this.root.and(t -> Arrays.stream(ids).anyMatch(i -> i == t.getComponentId())); + filter = filter.and(component -> contains(ids, component.componentId())); return this; } - /** - * Filters components by subcomponent ID. - * - * @param ids the subcomponent IDs to filter by - * @return the updated ComponentQuery - */ public ComponentQuery subComponentId(int... ids) { - this.root = this.root.and(t -> Arrays.stream(ids).anyMatch(i -> i == t.getSubComponentId())); + filter = filter.and(component -> contains(ids, component.subComponentId())); return this; } - /** - * Filters components by hidden status. - * - * @param hidden the hidden status to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery hidden(boolean hidden) { - this.root = this.root.and(t -> t.isHidden() == hidden); + public ComponentQuery type(int... types) { + filter = filter.and(component -> contains(types, component.type())); return this; } - /** - * Filters components by properties. - * - * @param properties the properties to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery properties(int... properties) { - this.root = this.root.and(t -> Arrays.stream(properties).anyMatch(i -> i == t.getProperties())); - return this; - } - - /** - * Filters components by font ID. - * - * @param fontIds the font IDs to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery fontId(int... fontIds) { - this.root = this.root.and(t -> Arrays.stream(fontIds).anyMatch(i -> i == t.getFontId())); - return this; - } - - /** - * Filters components by color. - * - * @param colors the colors to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery color(int... colors) { - this.root = this.root.and(t -> Arrays.stream(colors).anyMatch(i -> i == t.getColor())); - return this; - } - - /** - * Filters components by alpha. - * - * @param alphas the alphas to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery alpha(int... alphas) { - this.root = this.root.and(t -> Arrays.stream(alphas).anyMatch(i -> i == t.getAlpha())); - return this; - } - - /** - * Filters components by item ID. - * - * @param itemIds the item IDs to filter by - * @return the updated ComponentQuery - */ public ComponentQuery itemId(int... itemIds) { - this.root = this.root.and(t -> Arrays.stream(itemIds).anyMatch(i -> i == t.getItemId())); - return this; - } - - /** - * Filters components by item name using a custom string predicate. - * - * @param name the item name to filter by - * @param spred the predicate to match the item name - * @return the updated ComponentQuery - */ - public ComponentQuery itemName(String name, BiFunction spred) { - this.root = this.root.and(t -> { - var itemName = ConfigManager.getItemProvider().provide(t.getItemId()).getName(); - return spred.apply(name, itemName); - }); + filter = filter.and(component -> contains(itemIds, component.itemId())); return this; } - /** - * Filters components by item name. - * - * @param name the item name to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery itemName(String name) { - return itemName(name, String::contentEquals); - } - - /** - * Filters components by item amount. - * - * @param amounts the item amounts to filter by - * @return the updated ComponentQuery - */ public ComponentQuery itemAmount(int... amounts) { - this.root = this.root.and(t -> Arrays.stream(amounts).anyMatch(i -> i == t.getItemAmount())); + filter = filter.and(component -> contains(amounts, component.itemCount())); return this; } - /** - * Filters components by sprite ID. - * - * @param spriteIds the sprite IDs to filter by - * @return the updated ComponentQuery - */ public ComponentQuery spriteId(int... spriteIds) { - this.root = this.root.and(t -> Arrays.stream(spriteIds).anyMatch(i -> i == t.getSpriteId())); - return this; - } - - /** - * Filters components by text. - * - * @param spred the predicate to match text - * @param text the text to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery text(BiFunction spred, String... text) { - this.root = this.root.and(t -> Arrays.stream(text).anyMatch(i -> spred.apply(i, t.getText()))); - return this; - } - - /** - * Filters components by option-based text. - * - * @param spred the predicate to match option-based text - * @param text the option-based text to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery optionBasedText(BiFunction spred, String... text) { - this.root = this.root.and(t -> Arrays.stream(text).anyMatch(i -> spred.apply(i, t.getOptionBase()))); + filter = filter.and(component -> contains(spriteIds, component.spriteId())); + return this; + } + + public ComponentQuery text(BiFunction matcher, String... text) { + filter = filter.and(component -> { + String value = api.getComponentText(component.interfaceId(), component.componentId()); + if (value == null) { + return false; + } + for (String candidate : text) { + if (candidate != null && Boolean.TRUE.equals(matcher.apply(value, candidate))) { + return true; + } + } + return false; + }); return this; } - /** - * Filters components by options. - * - * @param spred the predicate to match options - * @param option the options to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery option(BiFunction spred, String... option) { - this.root = this.root.and(t -> { - var options = t.getOptions(); - return options != null && Arrays.stream(option).anyMatch(i -> i != null && options.stream().anyMatch(j -> j != null && spred.apply(i, j))); - }); + public ComponentQuery option(BiFunction matcher, String... options) { + filter = filter.and(component -> api.getComponentOptions(component.interfaceId(), component.componentId()).stream() + .anyMatch(option -> { + for (String candidate : options) { + if (candidate != null && Boolean.TRUE.equals(matcher.apply(option, candidate))) { + return true; + } + } + return false; + })); return this; } - /** - * Filters components by options using content equality. - * - * @param option the options to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery option(String... option) { - return option(String::contentEquals, option); + public ComponentQuery option(String... options) { + return option(String::contentEquals, options); } - /** - * Filters components by parameters. - * - * @param params the parameters to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery params(int... params) { - this.root = this.root.and(t -> Arrays.stream(params).anyMatch(i -> t.getParams().containsKey(i))); + public ComponentQuery itemName(String name, BiFunction matcher) { + filter = filter.and(component -> component.itemId() > -1 + && Boolean.TRUE.equals(matcher.apply(api.getItemType(component.itemId()).name(), name))); return this; } - /** - * Filters components by children IDs. - * - * @param ids the children IDs to filter by - * @return the updated ComponentQuery - */ - public ComponentQuery children(int... ids) { - this.root = this.root.and(t -> Arrays.stream(ids).anyMatch(i -> t.getChildren().stream().anyMatch(j -> j.getComponentId() == i))); - return this; + public ComponentQuery itemName(String name) { + return itemName(name, String::contentEquals); } - /** - * Retrieves the results of the query. - * - * @return a ResultSet containing the query results - */ @Override public ResultSet results() { - return new ResultSet<>( - Arrays.stream(ids) - .mapToObj(Interfaces::getInterface) // Map IDs to Interfaces - .filter(Objects::nonNull) // Filter out null interfaces - .flatMap(interfaceManager -> interfaceManager.getComponents().stream()) // Flatten components - .filter(this) // Apply the predicate (root.test) - .toList() // Collect the filtered components into a list - ); + List results = new ArrayList<>(); + for (int interfaceId : interfaceIds) { + results.addAll(api.queryComponents(ComponentFilter.builder() + .interfaceId(interfaceId) + .maxResults(500) + .build())); + } + results.removeIf(filter.negate()); + return new ResultSet<>(results); } - /** - * Returns an iterator over the query results. - * - * @return an Iterator over the query results - */ @Override public Iterator iterator() { return results().iterator(); } - /** - * Tests if a component matches the query predicate. - * - * @param comp the component to test - * @return true if the component matches, false otherwise - */ @Override - public boolean test(Component comp) { - return this.root.test(comp); + public boolean test(Component component) { + return filter.test(component); } -} \ No newline at end of file + private static boolean contains(int[] values, int actual) { + for (int value : values) { + if (value == actual) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/botwithus/xapi/query/GroundItemQuery.java b/src/main/java/net/botwithus/xapi/query/GroundItemQuery.java index 48c0aa8..023afff 100644 --- a/src/main/java/net/botwithus/xapi/query/GroundItemQuery.java +++ b/src/main/java/net/botwithus/xapi/query/GroundItemQuery.java @@ -1,303 +1,136 @@ package net.botwithus.xapi.query; -import net.botwithus.rs3.cache.assets.items.ItemDefinition; -import net.botwithus.rs3.cache.assets.items.StackType; -import net.botwithus.rs3.item.GroundItem; -import net.botwithus.rs3.world.Area; -import net.botwithus.rs3.world.Coordinate; -import net.botwithus.rs3.world.Distance; -import net.botwithus.rs3.world.World; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.entities.GroundItems; +import net.botwithus.xapi.XApi; import net.botwithus.xapi.query.base.Query; import net.botwithus.xapi.query.result.ResultSet; -import org.jetbrains.annotations.NotNull; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import java.util.function.BiFunction; import java.util.function.Predicate; +import java.util.regex.Pattern; -/** - * A query class for filtering and retrieving ground items based on various criteria. - */ -public class GroundItemQuery implements Query> { +public class GroundItemQuery implements Query> { - protected Predicate root; + private final GameAPI api; + private Predicate filter = item -> true; - /** - * Constructs a new GroundItemQuery with a default predicate. - */ - @SuppressWarnings("unused") - public GroundItemQuery() { - root = groundItem -> true; + private GroundItemQuery(GameAPI api) { + this.api = api; } - /** - * Creates a new GroundItemQuery instance. - * - * @return a new GroundItemQuery instance - */ public static GroundItemQuery newQuery() { - return new GroundItemQuery(); + return new GroundItemQuery(XApi.api()); } - /** - * Retrieves the results of the query. - * - * @return a ResultSet containing the query results - */ - @Override - public ResultSet results() { - return new ResultSet<>(World.getGroundItems().stream() - .flatMap(itemStack -> itemStack.getItems().stream()) - .filter(this) - .toList()); - } - - /** - * Returns an iterator over the query results. - * - * @return an Iterator over the query results - */ - @NotNull - @Override - public Iterator iterator() { - return results().iterator(); + public static GroundItemQuery newQuery(GameAPI api) { + return new GroundItemQuery(api); } - /** - * Tests if a ground item matches the query predicate. - * - * @param groundItem the ground item to test - * @return true if the ground item matches, false otherwise - */ - @Override - public boolean test(GroundItem groundItem) { - return this.root.test(groundItem); - } - - // ========== Item-based filtering methods ========== - - /** - * Filters ground items by item ID. - * - * @param ids the item IDs to filter by - * @return the updated GroundItemQuery - */ public GroundItemQuery id(int... ids) { - if (ids.length == 0) { - return this; - } - this.root = this.root.and(i -> Arrays.stream(ids).anyMatch(id -> id == i.getId())); + filter = filter.and(item -> matchesAny(item.itemId(), ids)); return this; } - /** - * Filters ground items by quantity using a predicate. - * - * @param spred the predicate to match quantities - * @param quantity the quantity to compare against - * @return the updated GroundItemQuery - */ - public GroundItemQuery quantity(BiFunction spred, int quantity) { - this.root = this.root.and(i -> spred.apply(i.getQuantity(), quantity)); + public GroundItemQuery quantity(BiFunction matcher, int quantity) { + filter = filter.and(item -> Boolean.TRUE.equals(matcher.apply(item.quantity(), quantity))); return this; } - /** - * Filters ground items by exact quantity. - * - * @param quantity the exact quantity to filter by - * @return the updated GroundItemQuery - */ public GroundItemQuery quantity(int quantity) { - return quantity((a, b) -> a.equals(b), quantity); - } - - /** - * Filters ground items by item types. - * - * @param itemTypes the item types to filter by - * @return the updated GroundItemQuery - */ - public GroundItemQuery itemTypes(ItemDefinition... itemTypes) { - if (itemTypes.length == 0) { - return this; - } - this.root = this.root.and(i -> Arrays.stream(itemTypes).anyMatch(itemType -> itemType.equals(i.getType()))); - return this; + return quantity(Integer::equals, quantity); } - /** - * Filters ground items by category. - * - * @param categories the categories to filter by - * @return the updated GroundItemQuery - */ public GroundItemQuery category(int... categories) { - if (categories.length == 0) { - return this; - } - this.root = this.root.and(i -> Arrays.stream(categories).anyMatch(category -> category == i.getCategory())); + filter = filter.and(item -> matchesAny(api.getItemType(item.itemId()).category(), categories)); return this; } - /** - * Filters ground items by name using a predicate. - * - * @param spred the predicate to match names - * @param names the names to filter by - * @return the updated GroundItemQuery - */ - public GroundItemQuery name(BiFunction spred, String... names) { - if (names.length == 0) { - return this; - } - this.root = this.root.and(i -> Arrays.stream(names).anyMatch(name -> spred.apply(i.getName(), name))); + public GroundItemQuery name(BiFunction matcher, String... names) { + filter = filter.and(item -> matchesAny(item.name(), matcher, names)); return this; } - /** - * Filters ground items by name using content equality. - * - * @param names the names to filter by - * @return the updated GroundItemQuery - */ public GroundItemQuery name(String... names) { return name(String::contentEquals, names); } - /** - * Filters ground items by name using regular expression patterns. - * - * @param patterns the regex patterns to filter names by - * @return the updated GroundItemQuery - */ - public GroundItemQuery name(java.util.regex.Pattern... patterns) { - if (patterns.length == 0) { - return this; - } - this.root = this.root.and(i -> { - String itemName = i.getName(); - return itemName != null && Arrays.stream(patterns).anyMatch(p -> p.matcher(itemName).matches()); - }); + public GroundItemQuery name(Pattern... patterns) { + filter = filter.and(item -> matchesAny(item.name(), patterns)); return this; } - /** - * Filters ground items by stack type. - * - * @param stackTypes the stack types to filter by - * @return the updated GroundItemQuery - */ - public GroundItemQuery stackType(StackType... stackTypes) { - if (stackTypes.length == 0) { - return this; - } - this.root = this.root.and(i -> Arrays.stream(stackTypes).anyMatch(stackType -> stackType == i.getStackType())); + public GroundItemQuery distance(double distance) { + filter = filter.and(item -> item.distanceToPlayer() <= distance); return this; } - // ========== GroundItem-specific filtering methods ========== - - /** - * Filters ground items by coordinate. - * - * @param coordinates the coordinates to filter by - * @return the updated GroundItemQuery - */ - public GroundItemQuery coordinate(Coordinate... coordinates) { - if (coordinates.length == 0) { - return this; - } - this.root = this.root.and(i -> Arrays.stream(coordinates).anyMatch(coord -> i.getStack().getCoordinate().equals(coord))); + public GroundItemQuery and(GroundItemQuery other) { + filter = filter.and(other.filter); return this; } - /** - * Filters ground items that are inside the given area. - * - * @param area the area to check - * @return the updated GroundItemQuery - */ - public GroundItemQuery inside(Area area) { - this.root = this.root.and(i -> area.contains(i.getStack().getCoordinate())); + public GroundItemQuery or(GroundItemQuery other) { + filter = filter.or(other.filter); return this; } - /** - * Filters ground items that are outside the given area. - * - * @param area the area to check - * @return the updated GroundItemQuery - */ - public GroundItemQuery outside(Area area) { - this.root = this.root.and(i -> !area.contains(i.getStack().getCoordinate())); + public GroundItemQuery invert() { + filter = filter.negate(); return this; } - /** - * Filters ground items by distance from the player. - * - * @param distance the maximum distance - * @return the updated GroundItemQuery - */ - public GroundItemQuery distance(double distance) { - this.root = this.root.and(i -> Distance.to(i.getStack().getCoordinate()) <= distance); - return this; + @Override + public ResultSet results() { + List results = new ArrayList<>(new GroundItems(api).query().all()); + results.removeIf(filter.negate()); + results.sort((a, b) -> Integer.compare(a.distanceToPlayer(), b.distanceToPlayer())); + return new ResultSet<>(results); } - /** - * Filters ground items by their validity status. - * - * @param valid true to filter for valid items, false for invalid items - * @return the updated GroundItemQuery - */ - public GroundItemQuery valid(boolean valid) { - this.root = this.root.and(i -> i.getStack().isValid() == valid); - return this; + @Override + public Iterator iterator() { + return results().iterator(); } - // ========== Utility methods ========== - - /** - * Combines the current query predicate with another using logical AND. - * - * @param other another GroundItemQuery to AND with - * @return the updated GroundItemQuery - */ - public GroundItemQuery and(GroundItemQuery other) { - this.root = this.root.and(other.root); - return this; + @Override + public boolean test(GroundItems.Entry groundItem) { + return filter.test(groundItem); } - /** - * Combines the current query predicate with another using logical OR. - * - * @param other another GroundItemQuery to OR with - * @return the updated GroundItemQuery - */ - public GroundItemQuery or(GroundItemQuery other) { - this.root = this.root.or(other.root); - return this; + private static boolean matchesAny(int actual, int... expected) { + for (int value : expected) { + if (value == actual) { + return true; + } + } + return false; } - /** - * Negates the current query predicate. - * - * @return the updated GroundItemQuery with negated predicate - */ - public GroundItemQuery invert() { - this.root = this.root.negate(); - return this; + private static boolean matchesAny(String actual, BiFunction matcher, String... expected) { + if (actual == null) { + return false; + } + for (String value : expected) { + if (value != null && Boolean.TRUE.equals(matcher.apply(actual, value))) { + return true; + } + } + return false; } - /** - * Marks the current GroundItemQuery and returns it. - * This method maintains the specific subtype. - * - * @return the current GroundItemQuery instance - */ - public GroundItemQuery mark() { - return this; + private static boolean matchesAny(String actual, Pattern... patterns) { + if (actual == null) { + return false; + } + for (Pattern pattern : patterns) { + if (pattern != null && pattern.matcher(actual).matches()) { + return true; + } + } + return false; } } diff --git a/src/main/java/net/botwithus/xapi/query/InventoryItemQuery.java b/src/main/java/net/botwithus/xapi/query/InventoryItemQuery.java index f54ba6a..773a541 100644 --- a/src/main/java/net/botwithus/xapi/query/InventoryItemQuery.java +++ b/src/main/java/net/botwithus/xapi/query/InventoryItemQuery.java @@ -1,75 +1,118 @@ package net.botwithus.xapi.query; -import net.botwithus.rs3.item.InventoryItem; -import net.botwithus.xapi.query.base.ItemQuery; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.model.InventoryItem; +import com.botwithus.bot.api.query.InventoryFilter; +import net.botwithus.xapi.XApi; +import net.botwithus.xapi.query.base.Query; import net.botwithus.xapi.query.result.ResultSet; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.regex.Pattern; -/** - * A query class for filtering and retrieving inventory items based on various criteria. - */ -public class InventoryItemQuery extends ItemQuery { +public class InventoryItemQuery implements Query> { - /** - * Constructs a new InvItemQuery with the specified inventory IDs. - * - * @param inventoryId the inventory IDs to query - */ - public InventoryItemQuery(int... inventoryId) { - super(inventoryId); + private final GameAPI api; + private final int[] inventoryIds; + private Predicate filter = item -> true; + + public InventoryItemQuery(GameAPI api, int... inventoryIds) { + this.api = api; + this.inventoryIds = inventoryIds; } - /** - * Creates a new InvItemQuery with the specified inventory IDs. - * - * @param inventoryIds the inventory IDs to query - * @return a new InvItemQuery instance - */ public static InventoryItemQuery newQuery(int... inventoryIds) { - return new InventoryItemQuery(inventoryIds); + return new InventoryItemQuery(XApi.api(), inventoryIds); + } + + public static InventoryItemQuery newQuery(GameAPI api, int... inventoryIds) { + return new InventoryItemQuery(api, inventoryIds); + } + + public InventoryItemQuery id(int... ids) { + filter = filter.and(item -> { + for (int id : ids) { + if (item.itemId() == id) { + return true; + } + } + return false; + }); + return this; + } + + public InventoryItemQuery slot(int... slots) { + filter = filter.and(item -> { + for (int slot : slots) { + if (item.slot() == slot) { + return true; + } + } + return false; + }); + return this; + } + + public InventoryItemQuery name(BiFunction matcher, String... names) { + filter = filter.and(item -> { + String name = item.itemId() > -1 ? api.getItemType(item.itemId()).name() : null; + if (name == null) { + return false; + } + for (String expected : names) { + if (expected != null && Boolean.TRUE.equals(matcher.apply(name, expected))) { + return true; + } + } + return false; + }); + return this; + } + + public InventoryItemQuery name(String... names) { + return name(String::contentEquals, names); + } + + public InventoryItemQuery name(Pattern... patterns) { + filter = filter.and(item -> { + String name = item.itemId() > -1 ? api.getItemType(item.itemId()).name() : null; + if (name == null) { + return false; + } + for (Pattern pattern : patterns) { + if (pattern != null && pattern.matcher(name).matches()) { + return true; + } + } + return false; + }); + return this; } - /** - * Retrieves the results of the query as a ResultSet. - * - * @return a ResultSet containing the filtered inventory items - */ @Override public ResultSet results() { - return new ResultSet<>(inventoryQuery.results().first().getItems().stream().filter(this).toList()); + List items = new ArrayList<>(); + for (int inventoryId : inventoryIds) { + items.addAll(api.queryInventoryItems(InventoryFilter.builder() + .inventoryId(inventoryId) + .nonEmpty(true) + .build())); + } + items.removeIf(filter.negate()); + return new ResultSet<>(items); } - /** - * Returns an iterator over the elements in the result set. - * - * @return an Iterator over the elements in the result set - */ @Override public Iterator iterator() { return results().iterator(); } - /** - * Tests if an inventory item matches the query predicate. - * - * @param inventoryItem the inventory item to test - * @return true if the inventory item matches, false otherwise - */ @Override public boolean test(InventoryItem inventoryItem) { - return this.root.test(inventoryItem); - } - - /** - * Filters inventory items by slot. - * - * @param slots the slots to filter by - * @return the updated InvItemQuery - */ - public InventoryItemQuery slot(int... slots) { - this.root = this.root.and(t -> Arrays.stream(slots).anyMatch(i -> i == t.getSlot())); - return this; + return filter.test(inventoryItem); } -} \ No newline at end of file +} diff --git a/src/main/java/net/botwithus/xapi/query/InventoryQuery.java b/src/main/java/net/botwithus/xapi/query/InventoryQuery.java index 6fcd4be..243e590 100644 --- a/src/main/java/net/botwithus/xapi/query/InventoryQuery.java +++ b/src/main/java/net/botwithus/xapi/query/InventoryQuery.java @@ -1,190 +1,158 @@ package net.botwithus.xapi.query; -import net.botwithus.rs3.cache.assets.inventories.InventoryDefinition; -import net.botwithus.rs3.inventories.Inventory; -import net.botwithus.rs3.inventories.InventoryManager; -import net.botwithus.rs3.item.Item; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.model.InventoryInfo; +import com.botwithus.bot.api.query.InventoryFilter; +import net.botwithus.xapi.XApi; import net.botwithus.xapi.query.base.Query; import net.botwithus.xapi.query.result.ResultSet; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Iterator; -import java.util.Objects; +import java.util.List; import java.util.function.BiFunction; import java.util.function.Predicate; -/** - * A query class for filtering and retrieving inventories based on various criteria. - */ -public class InventoryQuery implements Query> { +public class InventoryQuery implements Query> { - protected Predicate root; - private int[] ids; + private final GameAPI api; + private final int[] ids; + private Predicate filter; - /** - * Constructs a new InventoryQuery with the specified IDs. - * - * @param ids the IDs to query - */ - public InventoryQuery(int... ids) { + public InventoryQuery(GameAPI api, int... ids) { + this.api = api; this.ids = ids; - if (ids.length == 0) { - root = t -> true; - } else { - root = t -> Arrays.stream(ids).anyMatch(i -> i == t.getId()); - } + this.filter = info -> ids.length == 0 || contains(ids, info.inventoryId()); } - /** - * Retrieves the results of the query as a ResultSet. - * - * @return a ResultSet containing the filtered inventories - */ - @Override - public ResultSet results() { - return new ResultSet<>( - Arrays.stream(ids) - .mapToObj(InventoryManager::getInventory) // Map IDs to Inventories - .filter(Objects::nonNull) // Filter out null inventories - .filter(this) // Apply the predicate (root.test) - .toList() // Collect the filtered inventories into a list - ); - } - - /** - * Returns an iterator over the elements in the result set. - * - * @return an Iterator over the elements in the result set - */ - @Override - public Iterator iterator() { - return results().iterator(); + public static InventoryQuery newQuery(int... ids) { + return new InventoryQuery(XApi.api(), ids); } - /** - * Tests if an inventory matches the query predicate. - * - * @param inventory the inventory to test - * @return true if the inventory matches, false otherwise - */ - @Override - public boolean test(Inventory inventory) { - return this.root.test(inventory); - } - - /** - * Filters inventories by type. - * - * @param types the inventory types to filter by - * @return the updated InventoryQuery - */ - public InventoryQuery type(InventoryDefinition... types) { - root = root.and(t -> Arrays.stream(types).anyMatch(i -> i == t.getDefinition())); - return this; + public static InventoryQuery newQuery(GameAPI api, int... ids) { + return new InventoryQuery(api, ids); } - /** - * Filters inventories by full status. - * - * @param full the full status to filter by - * @return the updated InventoryQuery - */ public InventoryQuery isFull(boolean full) { - root = root.and(t -> t.isFull() == full); + filter = filter.and(info -> (info.capacity() > 0 && info.itemCount() >= info.capacity()) == full); return this; } - /** - * Filters inventories by the number of free slots using a custom function. - * - * @param func the function to compare free slots - * @param slots the number of slots to compare - * @return the updated InventoryQuery - */ - public InventoryQuery freeSlots(BiFunction func, int slots) { - root = root.and(t -> func.apply(t.freeSlots(), slots)); + public InventoryQuery freeSlots(BiFunction matcher, int slots) { + filter = filter.and(info -> Boolean.TRUE.equals(matcher.apply(info.capacity() - info.itemCount(), slots))); return this; } - /** - * Filters inventories by the number of free slots. - * - * @param slots the number of free slots to filter by - * @return the updated InventoryQuery - */ public InventoryQuery freeSlots(int slots) { - return freeSlots((a, b) -> a >= b, slots); + return freeSlots((actual, expected) -> actual >= expected, slots); } - /** - * Filters inventories by item IDs. - * - * @param itemIds the item IDs to filter by - * @return the updated InventoryQuery - */ public InventoryQuery contains(int... itemIds) { - root = root.and(t -> Arrays.stream(itemIds).anyMatch(t::contains)); + filter = filter.and(info -> { + for (int itemId : itemIds) { + if (!api.queryInventoryItems(InventoryFilter.builder() + .inventoryId(info.inventoryId()) + .itemId(itemId) + .nonEmpty(true) + .maxResults(1) + .build()).isEmpty()) { + return true; + } + } + return false; + }); return this; } - /** - * Filters inventories by containing any of the specified item names using a custom function. - * - * @param spred the function to compare item names - * @param names the item names to filter by - * @return the updated InventoryQuery - */ - public InventoryQuery contains(BiFunction spred, String... names) { - if (names.length == 0) { - return this; - } - this.root = this.root.and(t -> { - var itemNames = t.getItems().stream().map(Item::getName).toList(); - return Arrays.stream(names).anyMatch(i -> itemNames.stream().anyMatch(j -> spred.apply(i, j))); + public InventoryQuery containsAll(int... itemIds) { + filter = filter.and(info -> { + for (int itemId : itemIds) { + if (api.queryInventoryItems(InventoryFilter.builder() + .inventoryId(info.inventoryId()) + .itemId(itemId) + .nonEmpty(true) + .maxResults(1) + .build()).isEmpty()) { + return false; + } + } + return true; }); return this; } - /** - * Filters inventories by containing any of the specified item names. - * - * @param names the item names to filter by - * @return the updated InventoryQuery - */ - public InventoryQuery contains(String... names) { - return contains(String::contentEquals, names); + public InventoryQuery contains(BiFunction matcher, String... names) { + filter = filter.and(info -> api.queryInventoryItems(InventoryFilter.builder() + .inventoryId(info.inventoryId()) + .nonEmpty(true) + .build()).stream() + .map(item -> api.getItemType(item.itemId()).name()) + .anyMatch(name -> { + for (String candidate : names) { + if (candidate != null && Boolean.TRUE.equals(matcher.apply(name, candidate))) { + return true; + } + } + return false; + })); + return this; } - /** - * Filters inventories by containing all specified item IDs. - * - * @param itemIds the item IDs to filter by - * @return the updated InventoryQuery - */ - public InventoryQuery containsAll(int... itemIds) { - root = root.and(t -> Arrays.stream(itemIds).allMatch(t::contains)); - return this; + public InventoryQuery contains(String... names) { + return contains(String::contentEquals, names); } - /** - * Filters inventories by item categories. - * - * @param categories the item categories to filter by - * @return the updated InventoryQuery - */ public InventoryQuery containsCategory(int... categories) { - root = root.and(t -> Arrays.stream(categories).anyMatch(t::containsByCategory)); + filter = filter.and(info -> api.queryInventoryItems(InventoryFilter.builder() + .inventoryId(info.inventoryId()) + .nonEmpty(true) + .build()).stream() + .map(item -> api.getItemType(item.itemId()).category()) + .anyMatch(category -> contains(categories, category))); return this; } - /** - * Filters inventories by containing all specified item categories. - * - * @param categories the item categories to filter by - * @return the updated InventoryQuery - */ public InventoryQuery containsAllCategory(int... categories) { - root = root.and(t -> Arrays.stream(categories).allMatch(t::containsByCategory)); + filter = filter.and(info -> { + List present = api.queryInventoryItems(InventoryFilter.builder() + .inventoryId(info.inventoryId()) + .nonEmpty(true) + .build()).stream() + .map(item -> api.getItemType(item.itemId()).category()) + .toList(); + for (int category : categories) { + if (!present.contains(category)) { + return false; + } + } + return true; + }); return this; } -} \ No newline at end of file + + @Override + public ResultSet results() { + List results = new ArrayList<>(api.queryInventories()); + results.removeIf(filter.negate()); + return new ResultSet<>(results); + } + + @Override + public Iterator iterator() { + return results().iterator(); + } + + @Override + public boolean test(InventoryInfo inventoryInfo) { + return filter.test(inventoryInfo); + } + + private static boolean contains(int[] values, int actual) { + for (int value : values) { + if (value == actual) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/botwithus/xapi/query/NpcQuery.java b/src/main/java/net/botwithus/xapi/query/NpcQuery.java index 00a2291..0e8588d 100644 --- a/src/main/java/net/botwithus/xapi/query/NpcQuery.java +++ b/src/main/java/net/botwithus/xapi/query/NpcQuery.java @@ -1,53 +1,160 @@ package net.botwithus.xapi.query; -import net.botwithus.rs3.entities.PathingEntity; -import net.botwithus.rs3.world.World; -import net.botwithus.xapi.query.base.PathingEntityQuery; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.entities.Npc; +import com.botwithus.bot.api.entities.Npcs; +import net.botwithus.xapi.XApi; +import net.botwithus.xapi.query.base.Query; import net.botwithus.xapi.query.result.EntityResultSet; -import net.botwithus.xapi.query.result.ResultSet; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.regex.Pattern; -public class NpcQuery extends PathingEntityQuery { +public class NpcQuery implements Query> { + + private final GameAPI api; + private Predicate filter = npc -> true; + + private NpcQuery(GameAPI api) { + this.api = api; + } - /** - * Creates a new NpcQuery instance. - * - * @return a new NpcQuery instance - */ public static NpcQuery newQuery() { - return new NpcQuery(); + return new NpcQuery(XApi.api()); + } + + public static NpcQuery newQuery(GameAPI api) { + return new NpcQuery(api); + } + + public NpcQuery index(int... indices) { + filter = filter.and(npc -> matchesAny(npc.serverIndex(), indices)); + return this; + } + + public NpcQuery typeId(int... ids) { + filter = filter.and(npc -> matchesAny(npc.typeId(), ids)); + return this; + } + + public NpcQuery name(BiFunction matcher, String... names) { + filter = filter.and(npc -> matchesAny(npc.name(), matcher, names)); + return this; + } + + public NpcQuery name(String... names) { + return name(String::contentEquals, names); + } + + public NpcQuery name(Pattern... patterns) { + filter = filter.and(npc -> matchesAny(npc.name(), patterns)); + return this; + } + + public NpcQuery option(BiFunction matcher, String... options) { + filter = filter.and(npc -> npc.getOptions().stream().anyMatch(option -> matchesAny(option, matcher, options))); + return this; + } + + public NpcQuery option(String... options) { + return option(String::contentEquals, options); + } + + public NpcQuery option(Pattern... patterns) { + filter = filter.and(npc -> npc.getOptions().stream().anyMatch(option -> matchesAny(option, patterns))); + return this; + } + + public NpcQuery overheadText(String... overheadTexts) { + filter = filter.and(npc -> matchesAny(npc.getOverheadText(), String::contentEquals, overheadTexts)); + return this; + } + + public NpcQuery isMoving(boolean moving) { + filter = filter.and(npc -> npc.isMoving() == moving); + return this; + } + + public NpcQuery animationId(int... animationIds) { + filter = filter.and(npc -> matchesAny(npc.getAnimation(), animationIds)); + return this; + } + + public NpcQuery health(int min, int max) { + filter = filter.and(npc -> { + int hp = npc.getHealth(); + return hp >= min && hp <= max; + }); + return this; + } + + public NpcQuery and(NpcQuery other) { + filter = filter.and(other.filter); + return this; + } + + public NpcQuery or(NpcQuery other) { + filter = filter.or(other.filter); + return this; + } + + public NpcQuery invert() { + filter = filter.negate(); + return this; } - /** - * Retrieves the results of the query. - * - * @return a ResultSet containing the query results - */ @Override - public EntityResultSet results() { - return new EntityResultSet(World.getNpcs().stream().filter(this).toList()); + public EntityResultSet results() { + List results = new ArrayList<>(new Npcs(api).query().all()); + results.removeIf(filter.negate()); + results.sort((a, b) -> Integer.compare(a.distanceToPlayer(), b.distanceToPlayer())); + return new EntityResultSet<>(results); } - /** - * Returns an iterator over the query results. - * - * @return an Iterator over the query results - */ @Override - public Iterator iterator() { + public Iterator iterator() { return results().iterator(); } - /** - * Tests if a pathing entity matches the query predicate. - * - * @param pathingEntity the pathing entity to test - * @return true if the pathing entity matches, false otherwise - */ @Override - public boolean test(PathingEntity pathingEntity) { - return this.root.test(pathingEntity); + public boolean test(Npc npc) { + return filter.test(npc); } -} \ No newline at end of file + private static boolean matchesAny(int actual, int... expected) { + for (int value : expected) { + if (value == actual) { + return true; + } + } + return false; + } + + private static boolean matchesAny(String actual, BiFunction matcher, String... expected) { + if (actual == null) { + return false; + } + for (String value : expected) { + if (value != null && Boolean.TRUE.equals(matcher.apply(actual, value))) { + return true; + } + } + return false; + } + + private static boolean matchesAny(String actual, Pattern... patterns) { + if (actual == null) { + return false; + } + for (Pattern pattern : patterns) { + if (pattern != null && pattern.matcher(actual).matches()) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/botwithus/xapi/query/PlayerQuery.java b/src/main/java/net/botwithus/xapi/query/PlayerQuery.java new file mode 100644 index 0000000..16180eb --- /dev/null +++ b/src/main/java/net/botwithus/xapi/query/PlayerQuery.java @@ -0,0 +1,136 @@ +package net.botwithus.xapi.query; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.entities.Player; +import com.botwithus.bot.api.entities.Players; +import net.botwithus.xapi.XApi; +import net.botwithus.xapi.query.base.Query; +import net.botwithus.xapi.query.result.EntityResultSet; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public class PlayerQuery implements Query> { + + private final GameAPI api; + private Predicate filter = player -> true; + + private PlayerQuery(GameAPI api) { + this.api = api; + } + + public static PlayerQuery newQuery() { + return new PlayerQuery(XApi.api()); + } + + public static PlayerQuery newQuery(GameAPI api) { + return new PlayerQuery(api); + } + + public PlayerQuery name(BiFunction matcher, String... names) { + filter = filter.and(player -> matchesAny(player.name(), matcher, names)); + return this; + } + + public PlayerQuery name(String... names) { + return name(String::contentEquals, names); + } + + public PlayerQuery name(Pattern... patterns) { + filter = filter.and(player -> matchesAny(player.name(), patterns)); + return this; + } + + public PlayerQuery index(int... indices) { + filter = filter.and(player -> matchesAny(player.serverIndex(), indices)); + return this; + } + + public PlayerQuery isMoving(boolean moving) { + filter = filter.and(player -> player.isMoving() == moving); + return this; + } + + public PlayerQuery animationId(int... animationIds) { + filter = filter.and(player -> matchesAny(player.getAnimation(), animationIds)); + return this; + } + + public PlayerQuery health(int min, int max) { + filter = filter.and(player -> { + int hp = player.getHealth(); + return hp >= min && hp <= max; + }); + return this; + } + + public PlayerQuery and(PlayerQuery other) { + filter = filter.and(other.filter); + return this; + } + + public PlayerQuery or(PlayerQuery other) { + filter = filter.or(other.filter); + return this; + } + + public PlayerQuery invert() { + filter = filter.negate(); + return this; + } + + @Override + public EntityResultSet results() { + List results = new ArrayList<>(new Players(api).query().all()); + results.removeIf(filter.negate()); + results.sort((a, b) -> Integer.compare(a.distanceToPlayer(), b.distanceToPlayer())); + return new EntityResultSet<>(results); + } + + @Override + public Iterator iterator() { + return results().iterator(); + } + + @Override + public boolean test(Player player) { + return filter.test(player); + } + + private static boolean matchesAny(int actual, int... expected) { + for (int value : expected) { + if (value == actual) { + return true; + } + } + return false; + } + + private static boolean matchesAny(String actual, BiFunction matcher, String... expected) { + if (actual == null) { + return false; + } + for (String value : expected) { + if (value != null && Boolean.TRUE.equals(matcher.apply(actual, value))) { + return true; + } + } + return false; + } + + private static boolean matchesAny(String actual, Pattern... patterns) { + if (actual == null) { + return false; + } + for (Pattern pattern : patterns) { + if (pattern != null && pattern.matcher(actual).matches()) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/botwithus/xapi/query/SceneObjectQuery.java b/src/main/java/net/botwithus/xapi/query/SceneObjectQuery.java index fd272c1..29137db 100644 --- a/src/main/java/net/botwithus/xapi/query/SceneObjectQuery.java +++ b/src/main/java/net/botwithus/xapi/query/SceneObjectQuery.java @@ -1,195 +1,137 @@ package net.botwithus.xapi.query; -import net.botwithus.rs3.cache.assets.so.SceneObjectDefinition; -import net.botwithus.rs3.entities.SceneObject; -import net.botwithus.rs3.world.World; -import net.botwithus.xapi.query.base.EntityQuery; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.entities.SceneObject; +import com.botwithus.bot.api.entities.SceneObjects; +import net.botwithus.xapi.XApi; +import net.botwithus.xapi.query.base.Query; import net.botwithus.xapi.query.result.EntityResultSet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.regex.Pattern; -public class SceneObjectQuery extends EntityQuery { +public class SceneObjectQuery implements Query> { - private static final Logger logger = LoggerFactory.getLogger(SceneObjectQuery.class); + private final GameAPI api; + private Predicate filter = object -> true; + + private SceneObjectQuery(GameAPI api) { + this.api = api; + } - /** - * Creates a new SceneObjectQuery instance. - * - * @return a new SceneObjectQuery instance - */ public static SceneObjectQuery newQuery() { - return new SceneObjectQuery(); + return new SceneObjectQuery(XApi.api()); } - /** - * Retrieves the results of the query. - * - * @return a ResultSet containing the query results - */ - @Override - public EntityResultSet results() { - return new EntityResultSet<>(World.getSceneObjects().stream().filter(this).toList()); + public static SceneObjectQuery newQuery(GameAPI api) { + return new SceneObjectQuery(api); } - /** - * Returns an iterator over the query results. - * - * @return an Iterator over the query results - */ - @Override - public Iterator iterator() { - return results().iterator(); + public SceneObjectQuery typeId(int... typeIds) { + filter = filter.and(object -> matchesAny(object.typeId(), typeIds)); + return this; } - /** - * Tests if a scene object matches the query predicate. - * - * @param sceneObject the scene object to test - * @return true if the scene object matches, false otherwise - */ - @Override - public boolean test(SceneObject sceneObject) { - return this.root.test(sceneObject); + public SceneObjectQuery hidden(boolean hidden) { + filter = filter.and(object -> object.isHidden() == hidden); + return this; } - /** - * Filters scene objects by type ID. - * - * @param typeIds the type IDs to filter by - * @return the updated SceneObjectQuery - */ - public SceneObjectQuery typeId(int... typeIds) { - if (typeIds.length == 0) { - return this; - } - this.root = this.root.and(t -> Arrays.stream(typeIds).anyMatch(i -> t.getTypeId() == i)); + public SceneObjectQuery name(BiFunction matcher, String... names) { + filter = filter.and(object -> matchesAny(object.name(), matcher, names)); return this; } - /** - * Filters scene objects by animation ID. - * - * @param animations the animation IDs to filter by - * @return the updated SceneObjectQuery - */ - public SceneObjectQuery animation(int... animations) { - if (animations.length == 0) { - return this; - } - this.root = this.root.and(t -> Arrays.stream(animations).anyMatch(i -> t.getAnimationId() == i)); + public SceneObjectQuery name(String... names) { + return name(String::contentEquals, names); + } + + public SceneObjectQuery name(Pattern... patterns) { + filter = filter.and(object -> matchesAny(object.name(), patterns)); return this; } - /** - * Filters scene objects by hidden status. - * - * @param hidden the hidden status to filter by - * @return the updated SceneObjectQuery - */ - public SceneObjectQuery hidden(boolean hidden) { - this.root = this.root.and(t -> t.isHidden() == hidden); + public SceneObjectQuery option(BiFunction matcher, String... options) { + filter = filter.and(object -> object.getOptions().stream().anyMatch(option -> matchesAny(option, matcher, options))); return this; } - /** - * Filters scene objects by multiple types. - * - * @param sceneObjectDefinitions the location types to filter by - * @return the updated SceneObjectQuery - */ - public SceneObjectQuery multiType(SceneObjectDefinition... sceneObjectDefinitions) { - if (sceneObjectDefinitions.length == 0) { - return this; - } - this.root = this.root.and(t -> Arrays.stream(sceneObjectDefinitions).anyMatch(i -> t.getMultiType() == i)); + public SceneObjectQuery option(String... options) { + return option(String::contentEquals, options); + } + + public SceneObjectQuery option(Pattern... patterns) { + filter = filter.and(object -> object.getOptions().stream().anyMatch(option -> matchesAny(option, patterns))); return this; } - /** - * Filters scene objects by name using a predicate. - * - * @param spred the predicate to match names - * @param names the names to filter by - * @return the updated SceneObjectQuery - */ - public SceneObjectQuery name(BiFunction spred, String... names) { - if (names.length == 0) { - return this; - } - this.root = this.root.and(t -> Arrays.stream(names).anyMatch(i -> spred.apply(i, t.getName()))); + public SceneObjectQuery and(SceneObjectQuery other) { + filter = filter.and(other.filter); return this; } - /** - * Filters scene objects by name using content equality. - * - * @param names the names to filter by - * @return the updated SceneObjectQuery - */ - public SceneObjectQuery name(String... names) { - return name(String::contentEquals, names); + public SceneObjectQuery or(SceneObjectQuery other) { + filter = filter.or(other.filter); + return this; } - public SceneObjectQuery name(java.util.regex.Pattern... patterns) { - if (patterns.length == 0) { - return this; - } - this.root = this.root.and(t -> { - String objName = t.getName(); - return objName != null && Arrays.stream(patterns).anyMatch(p -> p.matcher(objName).matches()); - }); + public SceneObjectQuery invert() { + filter = filter.negate(); return this; } - /** - * Filters scene objects by options using a predicate. - * - * @param spred the predicate to match options - * @param options the options to filter by - * @return the updated SceneObjectQuery - */ - public SceneObjectQuery option(BiFunction spred, String... options) { - if (options.length == 0) { - return this; + @Override + public EntityResultSet results() { + List results = new ArrayList<>(new SceneObjects(api).query().all()); + results.removeIf(filter.negate()); + results.sort((a, b) -> Integer.compare(a.distanceToPlayer(), b.distanceToPlayer())); + return new EntityResultSet<>(results); + } + + @Override + public Iterator iterator() { + return results().iterator(); + } + + @Override + public boolean test(SceneObject sceneObject) { + return filter.test(sceneObject); + } + + private static boolean matchesAny(int actual, int... expected) { + for (int value : expected) { + if (value == actual) { + return true; + } } - this.root = this.root.and(t -> { - var objOptions = t.getOptions(); - return objOptions != null && Arrays.stream(options).anyMatch(i -> i != null && objOptions.stream().anyMatch(j -> j != null && spred.apply(i, j))); - }); - return this; + return false; } - /** - * Filters scene objects by options using content equality. - * - * @param option the options to filter by - * @return the updated SceneObjectQuery - */ - public SceneObjectQuery option(String... option) { - return option(String::contentEquals, option); - } - - /** - * Filters scene objects by options using regular expression patterns. - * - * @param patterns the regex patterns to filter options by - * @return the updated SceneObjectQuery - */ - public SceneObjectQuery option(java.util.regex.Pattern... patterns) { - if (patterns.length == 0) { - return this; + private static boolean matchesAny(String actual, BiFunction matcher, String... expected) { + if (actual == null) { + return false; } - this.root = this.root.and(t -> { - var objOptions = t.getOptions(); - return objOptions != null && objOptions.stream().anyMatch(opt -> - Arrays.stream(patterns).anyMatch(p -> p.matcher(opt).matches()) - ); - }); - return this; + for (String value : expected) { + if (value != null && Boolean.TRUE.equals(matcher.apply(actual, value))) { + return true; + } + } + return false; } -} \ No newline at end of file + private static boolean matchesAny(String actual, Pattern... patterns) { + if (actual == null) { + return false; + } + for (Pattern pattern : patterns) { + if (pattern != null && pattern.matcher(actual).matches()) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/botwithus/xapi/query/base/EntityQuery.java b/src/main/java/net/botwithus/xapi/query/base/EntityQuery.java deleted file mode 100644 index 55128e0..0000000 --- a/src/main/java/net/botwithus/xapi/query/base/EntityQuery.java +++ /dev/null @@ -1,149 +0,0 @@ -package net.botwithus.xapi.query.base; - -import net.botwithus.rs3.entities.Entity; -import net.botwithus.rs3.entities.EntityType; -import net.botwithus.rs3.world.Vector3f; -import net.botwithus.rs3.world.Area; -import net.botwithus.rs3.world.Coordinate; -import net.botwithus.rs3.world.Distance; -import net.botwithus.xapi.query.result.EntityResultSet; - -import java.util.Arrays; -import java.util.function.Predicate; - -public abstract class EntityQuery implements Query> { - - protected Predicate root; - - /** - * Constructs a new EntityQuery with a default predicate. - */ - @SuppressWarnings("unused") - public EntityQuery() { - root = t -> true; - } - - /** - * Filters entities by type. - * - * @param entityType the entity types to filter by - * @return the updated EntityQuery - */ - @SuppressWarnings("unchecked") - public > Q type(EntityType... entityType) { - this.root = this.root.and(t -> Arrays.stream(entityType).anyMatch(i -> t.getType() == i)); - return (Q) this; - } - - /** - * Filters entities by coordinate. - * - * @param coordinate the coordinates to filter by - * @return the updated EntityQuery - */ - @SuppressWarnings("unchecked") - public > Q coordinate(Coordinate... coordinate) { - this.root = this.root.and(t -> Arrays.stream(coordinate).anyMatch(i -> t.getCoordinate().equals(i))); - return (Q) this; - } - - /** - * Filters entities by direction. - * - * @param vector3f the directions to filter by - * @return the updated EntityQuery - */ - @SuppressWarnings("unchecked") - public > Q direction(Vector3f... vector3f) { - this.root = this.root.and(t -> Arrays.stream(vector3f).anyMatch(i -> t.getDirection().equals(i))); - return (Q) this; - } - - /** - * Filters entities by valid status. - * - * @param valid the valid status to filter by - * @return the updated EntityQuery - */ - @SuppressWarnings("unchecked") - public > Q valid(boolean valid) { - this.root = this.root.and(t -> t.isValid() == valid); - return (Q) this; - } - - /** - * Filters entities that are inside the given area. - * - * @param area the area to check - * @return the updated EntityQuery - */ - @SuppressWarnings("unchecked") - public > Q inside(Area area) { - this.root = this.root.and(t -> area.contains(t.getCoordinate())); - return (Q) this; - } - - /** - * Filters entities that are outside the given area. - * - * @param area the area to check - * @return the updated EntityQuery - */ - @SuppressWarnings("unchecked") - public > Q outside(Area area) { - this.root = this.root.and(t -> !area.contains(t.getCoordinate())); - return (Q) this; - } - - @SuppressWarnings("unchecked") - public > Q distance(double distance) { - this.root = this.root.and(t -> Distance.to(t) <= distance); - return (Q) this; - } - - /** - * Combines the current query predicate with another using logical AND. - * - * @param other another EntityQuery to AND with - * @return the updated EntityQuery - */ - @SuppressWarnings("unchecked") - public > Q and(EntityQuery other) { - this.root = this.root.and(other.root); - return (Q) this; - } - - /** - * Combines the current query predicate with another using logical OR. - * - * @param other another EntityQuery to OR with - * @return the updated EntityQuery - */ - @SuppressWarnings("unchecked") - public > Q or(EntityQuery other) { - this.root = this.root.or(other.root); - return (Q) this; - } - - /** - * Negates the current query predicate. - * - * @return the updated EntityQuery with negated predicate - */ - @SuppressWarnings("unchecked") - public > Q inverse() { - this.root = this.root.negate(); - return (Q) this; - } - - /** - * Marks the current EntityQuery and returns it. - * This method uses a generic return type to maintain the specific subtype. - * - * @return the current EntityQuery instance - */ - @SuppressWarnings("unchecked") - public > Q mark() { - return (Q) this; - } -} \ No newline at end of file diff --git a/src/main/java/net/botwithus/xapi/query/base/ItemQuery.java b/src/main/java/net/botwithus/xapi/query/base/ItemQuery.java deleted file mode 100644 index 5faf6ae..0000000 --- a/src/main/java/net/botwithus/xapi/query/base/ItemQuery.java +++ /dev/null @@ -1,76 +0,0 @@ -package net.botwithus.xapi.query.base; - -import net.botwithus.rs3.cache.assets.items.ItemDefinition; -import net.botwithus.rs3.cache.assets.items.StackType; -import net.botwithus.rs3.item.Item; -import net.botwithus.xapi.query.InventoryQuery; -import net.botwithus.xapi.query.result.ResultSet; - -import java.util.Arrays; -import java.util.function.BiFunction; -import java.util.function.Predicate; - -public abstract class ItemQuery> implements Query> { - - protected Predicate root; - - protected InventoryQuery inventoryQuery; - - public ItemQuery(int... inventoryId) { - inventoryQuery = new InventoryQuery(inventoryId); - root = item -> true; // Initialize with a predicate that accepts all items - } - - @SuppressWarnings("unchecked") - public T id(int... ids) { - root = root.and(i -> Arrays.stream(ids).anyMatch(id -> id == i.getId())); - return (T) this; - } - - @SuppressWarnings("unchecked") - public T quantity(BiFunction spred, int quantity) { - root = root.and(i -> spred.apply(i.getQuantity(), quantity)); - return (T) this; - } - - public T quantity(int quantity) { - return quantity((a, b) -> a == b, quantity); - } - - @SuppressWarnings("unchecked") - public T itemTypes(ItemDefinition... itemTypes) { - root = root.and(i -> Arrays.stream(itemTypes).anyMatch(itemType -> itemType == i.getType())); - return (T) this; - } - - @SuppressWarnings("unchecked") - public T category(int... categories) { - root = root.and(i -> Arrays.stream(categories).anyMatch(category -> category == i.getCategory())); - return (T) this; - } - - @SuppressWarnings("unchecked") - public T name(BiFunction spred, String... names) { - root = root.and(i -> Arrays.stream(names).anyMatch(name -> spred.apply(i.getName(), name))); - return (T) this; - } - - public T name(String... names) { - return name(String::contentEquals, names); - } - - @SuppressWarnings("unchecked") - public T name(java.util.regex.Pattern... patterns) { - root = root.and(i -> { - String itemName = i.getName(); - return itemName != null && Arrays.stream(patterns).anyMatch(p -> p.matcher(itemName).matches()); - }); - return (T) this; - } - - @SuppressWarnings("unchecked") - public T stackType(StackType... stackTypes) { - root = root.and(i -> Arrays.stream(stackTypes).anyMatch(stackType -> stackType == i.getStackType())); - return (T) this; - } -} diff --git a/src/main/java/net/botwithus/xapi/query/base/PathingEntityQuery.java b/src/main/java/net/botwithus/xapi/query/base/PathingEntityQuery.java deleted file mode 100644 index 8d67bd4..0000000 --- a/src/main/java/net/botwithus/xapi/query/base/PathingEntityQuery.java +++ /dev/null @@ -1,219 +0,0 @@ -package net.botwithus.xapi.query.base; - -import net.botwithus.rs3.entities.EntityType; -import net.botwithus.rs3.entities.PathingEntity; -import net.botwithus.xapi.query.ComponentQuery; - -import java.util.Arrays; -import java.util.function.BiFunction; - -public abstract class PathingEntityQuery extends EntityQuery { - - /** - * Filters pathing entities by index. - * - * @param indices the indices to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery index(int... indices) { - if (indices.length == 0) { - return this; - } - this.root = this.root.and(t -> Arrays.stream(indices).anyMatch(i -> t.getIndex() == i)); - return this; - } - - /** - * Filters pathing entities by type ID. - * - * @param typeIds the type IDs to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery typeId(int... typeIds) { - if (typeIds.length == 0) { - return this; - } - this.root = this.root.and(t -> Arrays.stream(typeIds).anyMatch(i -> t.getTypeId() == i)); - return this; - } - - /** - * Filters pathing entities by name. - * - * @param names the names to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery name(String... names) { - if (names.length == 0) { - return this; - } - this.root = this.root.and(t -> Arrays.stream(names).anyMatch(n -> t.getName().equals(n))); - return this; - } - - public PathingEntityQuery name(java.util.regex.Pattern... patterns) { - if (patterns.length == 0) { - return this; - } - this.root = this.root.and(t -> { - String entityName = t.getName(); - return entityName != null && Arrays.stream(patterns).anyMatch(p -> p.matcher(entityName).matches()); - }); - return this; - } - - /** - * Filters pathing entities by overhead text. - * - * @param overheadTexts the overhead texts to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery overheadText(String... overheadTexts) { - if (overheadTexts.length == 0) { - return this; - } - this.root = this.root.and(t -> Arrays.stream(overheadTexts).anyMatch(n -> t.getOverheadText().equals(n))); - return this; - } - - /** - * Filters pathing entities by moving status. - * - * @param isMoving the moving status to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery isMoving(boolean isMoving) { - this.root = this.root.and(t -> t.isMoving() == isMoving); - return this; - } - - /** - * Filters pathing entities by animation ID. - * - * @param animationIds the animation IDs to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery animationId(int... animationIds) { - if (animationIds.length == 0) { - return this; - } - this.root = this.root.and(t -> Arrays.stream(animationIds).anyMatch(i -> t.getAnimationId() == i)); - return this; - } - - /** - * Filters pathing entities by stance ID. - * - * @param stanceIds the stance IDs to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery stanceId(int... stanceIds) { - if (stanceIds.length == 0) { - return this; - } - this.root = this.root.and(t -> Arrays.stream(stanceIds).anyMatch(i -> t.getStanceId() == i)); - return this; - } - - /** - * Filters pathing entities by health range. - * - * @param min the minimum health - * @param max the maximum health - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery health(int min, int max) { - this.root = this.root.and(t -> t.getHealth() >= min && t.getHealth() <= max); - return this; - } - - /** - * Filters pathing entities by following entity type and index. - * - * @param type the entity type to filter by - * @param index the entity index to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery following(EntityType type, int index) { - this.root = this.root.and(t -> t.getFollowingType() == type && t.getFollowingIndex() == index); - return this; - } - - /** - * Filters pathing entities by following specific entities. - * - * @param entity the entities to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery following(PathingEntity... entity) { - this.root = this.root.and(t -> Arrays.stream(entity).anyMatch(e -> t.getFollowingType() == e.getType() && t.getFollowingIndex() == e.getIndex())); - return this; - } - - /** - * Filters pathing entities by headbars. - * - * @param headbars the headbars to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery headbars(int... headbars) { - if (headbars.length == 0) { - return this; - } - this.root = this.root.and(t -> Arrays.stream(headbars).anyMatch(i -> t.getHeadbar(i) != null)); - return this; - } - - /** - * Filters pathing entities by hitmarks. - * - * @param hitmarks the hitmarks to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery hitmarks(int... hitmarks) { - if (hitmarks.length == 0) { - return this; - } - this.root = this.root.and(t -> Arrays.stream(hitmarks).anyMatch(i -> t.getHitmark(i) != null)); - return this; - } - - /** - * Filters PathingEntities by options. - * - * @param spred the predicate to match options - * @param option the options to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery option(BiFunction spred, String... option) { - this.root = this.root.and(t -> { - var options = t.getOptions(); - return options != null && Arrays.stream(option).anyMatch(i -> i != null && options.stream().anyMatch(j -> j != null && spred.apply(i, j))); - }); - return this; - } - - /** - * Filters PathingEntities by options using content equality. - * - * @param option the options to filter by - * @return the updated PathingEntityQuery - */ - public PathingEntityQuery option(String... option) { - return option(String::contentEquals, option); - } - - - public PathingEntityQuery option(java.util.regex.Pattern... patterns) { - if (patterns.length == 0) { - return this; - } - this.root = this.root.and(t -> { - var options = t.getOptions(); - return options != null && options.stream().anyMatch(opt -> - Arrays.stream(patterns).anyMatch(p -> p.matcher(opt).matches()) - ); - }); - return this; - } -} \ No newline at end of file diff --git a/src/main/java/net/botwithus/xapi/query/result/EntityResultSet.java b/src/main/java/net/botwithus/xapi/query/result/EntityResultSet.java index 2b58e52..b29cdf4 100644 --- a/src/main/java/net/botwithus/xapi/query/result/EntityResultSet.java +++ b/src/main/java/net/botwithus/xapi/query/result/EntityResultSet.java @@ -1,88 +1,104 @@ package net.botwithus.xapi.query.result; -import net.botwithus.rs3.entities.Entity; -import net.botwithus.rs3.entities.LocalPlayer; -import net.botwithus.rs3.world.Coordinate; -import net.botwithus.rs3.world.Distance; +import com.botwithus.bot.api.entities.EntityContext; +import com.botwithus.bot.api.entities.GroundItems; +import com.botwithus.bot.api.model.LocalPlayer; +import net.botwithus.xapi.util.BwuDistance; +import net.botwithus.xapi.util.position.Positionable; import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.Objects; -public class EntityResultSet extends ResultSet { +public class EntityResultSet extends ResultSet { - /** - * Constructs an EntityResultSet with the given results. - * - * @param results the list of entities - */ public EntityResultSet(List results) { super(results); } - /** - * Finds the nearest entity to the given coordinate. - * - * @param coordinate the coordinate to compare - * @return the nearest entity, or null if no entities are found - */ - public T nearestTo(Coordinate coordinate) { - List copy = new ArrayList<>(results); - List sorted = copy.stream() - .filter(Objects::nonNull) - .sorted(Comparator.comparingDouble(o -> Distance.between(o, coordinate))) - .toList(); - if (!sorted.isEmpty()) { - return sorted.get(0); + public T nearestTo(Positionable other) { + if (other == null || results.isEmpty()) { + return null; } - return null; + return results.stream() + .min(Comparator.comparingInt(candidate -> BwuDistance.distance(asPositionable(candidate), other))) + .orElse(null); + } + + public T nearest() { + return first(); } - /** - * Removes all entities in the given result set from this result set. - * - * @param set the result set to remove - * @return a new EntityResultSet with the entities removed - */ public EntityResultSet removeAll(ResultSet set) { List copy = new ArrayList<>(results); copy.removeAll(set.results); return new EntityResultSet<>(copy); } - /** - * Removes the specified entity from this result set. - * - * @param toRemove the entity to remove - * @return a new EntityResultSet with the entity removed - */ public EntityResultSet remove(T toRemove) { List copy = new ArrayList<>(results); copy.remove(toRemove); return new EntityResultSet<>(copy); } - /** - * Finds the nearest entity to the given entity. - * - * @param entity the entity to compare - * @return the nearest entity, or null if no entities are found - */ - public T nearestTo(Entity entity) { - return entity != null && entity.getCoordinate() != null ? nearestTo(entity.getCoordinate()) : null; - } + private static Positionable asPositionable(Object value) { + if (value instanceof Positionable positionable) { + return positionable; + } + if (value instanceof EntityContext entity) { + return new Positionable() { + @Override + public int x() { + return entity.tileX(); + } - /** - * Finds the nearest entity to the local player. - * - * @return the nearest entity, or null if no entities are found - */ - public T nearest() { - var player = LocalPlayer.self(); - if (player != null) { - return nearestTo(player); + @Override + public int y() { + return entity.tileY(); + } + + @Override + public int plane() { + return entity.plane(); + } + }; + } + if (value instanceof GroundItems.Entry entry) { + return new Positionable() { + @Override + public int x() { + return entry.tileX(); + } + + @Override + public int y() { + return entry.tileY(); + } + + @Override + public int plane() { + return entry.plane(); + } + }; + } + if (value instanceof LocalPlayer player) { + return new Positionable() { + @Override + public int x() { + return player.tileX(); + } + + @Override + public int y() { + return player.tileY(); + } + + @Override + public int plane() { + return player.plane(); + } + }; } - return null; + throw new IllegalArgumentException("Unsupported entity result type: " + value); } -} \ No newline at end of file +} diff --git a/src/main/java/net/botwithus/xapi/query/result/ResultSet.java b/src/main/java/net/botwithus/xapi/query/result/ResultSet.java index fab03e6..59640a4 100644 --- a/src/main/java/net/botwithus/xapi/query/result/ResultSet.java +++ b/src/main/java/net/botwithus/xapi/query/result/ResultSet.java @@ -1,97 +1,43 @@ package net.botwithus.xapi.query.result; -import net.botwithus.util.Rand; - import java.util.Iterator; import java.util.List; +import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Stream; public class ResultSet implements Iterable { - private static final java.util.Random RANDOM = Rand.getSecureThreadLocalRandom(); protected final List results; - /** - * Constructs a ResultSet with the given results. - * - * @param results the list of results - */ public ResultSet(List results) { - this.results = results; + this.results = List.copyOf(results); } - /** - * Retrieves the first element in the result set. - * - * @return the first element, or null if the result set is empty - */ public T first() { - if (results.isEmpty()) { - return null; - } - return results.get(0); + return results.isEmpty() ? null : results.getFirst(); } - /** - * Retrieves the last element in the result set. - * - * @return the last element, or null if the result set is empty - */ public T last() { - if (results.isEmpty()) { - return null; - } - return results.get(results.size() - 1); + return results.isEmpty() ? null : results.getLast(); } - /** - * Retrieves a random element from the result set. - * - * @return a random element, or null if the result set is empty - */ public T random() { - int size = results.size(); - if (size == 0) { - return null; - } else { - // get a random element at the index between [0, size) - return results.get(RANDOM.nextInt(size)); - } - } - - /** - * Returns an iterator over the elements in the result set. - * - * @return an Iterator over the elements in the result set - */ - @Override - public Iterator iterator() { - return results.iterator(); + return results.isEmpty() ? null : results.get(ThreadLocalRandom.current().nextInt(results.size())); } - /** - * Returns a sequential Stream with the elements in the result set. - * - * @return a Stream with the elements in the result set - */ public Stream stream() { return results.stream(); } - /** - * Returns the number of elements in the result set. - * - * @return the number of elements in the result set - */ public int size() { return results.size(); } - /** - * Checks if the result set is empty. - * - * @return true if the result set is empty, false otherwise - */ public boolean isEmpty() { return results.isEmpty(); } -} \ No newline at end of file + + @Override + public Iterator iterator() { + return results.iterator(); + } +} diff --git a/src/main/java/net/botwithus/xapi/script/BwuScript.java b/src/main/java/net/botwithus/xapi/script/BwuScript.java index 79f79c3..c4d9e1c 100644 --- a/src/main/java/net/botwithus/xapi/script/BwuScript.java +++ b/src/main/java/net/botwithus/xapi/script/BwuScript.java @@ -1,17 +1,16 @@ package net.botwithus.xapi.script; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.ScriptContext; +import com.botwithus.bot.api.model.InventoryItem; +import com.botwithus.bot.api.model.LocalPlayer; +import com.botwithus.bot.api.query.InventoryFilter; +import net.botwithus.xapi.game.inventory.BwuBackpack; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; - -import net.botwithus.events.EventInfo; -import net.botwithus.rs3.entities.LocalPlayer; -import net.botwithus.rs3.inventories.events.InventoryEvent; import net.botwithus.scripts.Info; -import net.botwithus.ui.workspace.ExtInfo; import net.botwithus.ui.workspace.Workspace; -import net.botwithus.ui.workspace.WorkspaceExtension; -import net.botwithus.xapi.script.permissive.node.Branch; import net.botwithus.xapi.script.permissive.base.PermissiveScript; import net.botwithus.xapi.script.ui.BwuGraphicsContext; import net.botwithus.xapi.script.ui.interfaces.BuildableUI; @@ -27,38 +26,101 @@ import java.util.HashMap; import java.util.Map; - public abstract class BwuScript extends PermissiveScript { - private BwuGraphicsContext graphicsContext = null; - public Stopwatch STOPWATCH; + private BwuGraphicsContext graphicsContext; + private Map previousInventory = Map.of(); + public Stopwatch STOPWATCH; public LocalPlayer player; public BotStat botStatInfo = new BotStat(); public String getName() { - return this.getClass().getSimpleName(); + return getClass().getSimpleName(); + } + + public Info getInfo() { + return getClass().getAnnotation(Info.class); } - @Override public void onDraw(Workspace workspace) { - super.onDraw(workspace); if (graphicsContext == null) { graphicsContext = new BwuGraphicsContext(this, workspace); } - graphicsContext.draw(); } public abstract void onDrawConfig(Workspace workspace); @Override - public void onInitialize() { + protected void onInitialize() { super.onInitialize(); try { performLoadPersistentData(); } catch (Exception e) { println("Failed to load persistent data"); + e.printStackTrace(); + } + } + + @Override + public boolean onPreTick() { + player = gameApi().getLocalPlayer(); + pollInventoryEvents(); + return super.onPreTick() && player != null; + } + + @Override + public void onActivation() { + super.onActivation(); + if (STOPWATCH == null) { + STOPWATCH = Stopwatch.startNew(); + } else { + STOPWATCH.resume(); + } + } + + @Override + public void onDeactivation() { + super.onDeactivation(); + if (STOPWATCH != null) { + STOPWATCH.pause(); } + performSavePersistentData(); + } + + protected GameAPI gameApi() { + return context().getGameAPI(); + } + + protected GameAPI gameApi(String clientName) { + return net.botwithus.xapi.XApi.api(clientName); + } + + protected ScriptContext scriptContext() { + return context(); + } + + private void pollInventoryEvents() { + Map current = new HashMap<>(); + for (InventoryItem item : gameApi().queryInventoryItems( + InventoryFilter.builder() + .inventoryId(BwuBackpack.INVENTORY_ID) + .nonEmpty(false) + .build())) { + current.put(item.slot(), item); + InventoryItem previous = previousInventory.get(item.slot()); + if (previous == null) { + continue; + } + if (previous.itemId() <= -1 && item.itemId() > -1) { + onItemAcquired(item); + } else if (previous.itemId() > -1 && item.itemId() <= -1) { + onItemRemoved(previous); + } else if (previous.itemId() != item.itemId() || previous.quantity() != item.quantity()) { + onItemChange(previous, item); + } + } + previousInventory = current; } public void performSavePersistentData() { @@ -68,7 +130,6 @@ public void performSavePersistentData() { Path path = Paths.get(System.getProperty("user.home"), ".botwithus", "configs", getName() + "_settings.json"); Files.createDirectories(path.getParent()); - try (Writer writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { new GsonBuilder().setPrettyPrinting().create().toJson(obj, writer); } @@ -81,12 +142,13 @@ public void performSavePersistentData() { public void performLoadPersistentData() { try { Path path = Paths.get(System.getProperty("user.home"), ".botwithus", "configs", getName() + "_settings.json"); - if (Files.exists(path)) { - try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { - JsonObject obj = new Gson().fromJson(reader, JsonObject.class); - if (obj != null) { - loadPersistentData(obj); - } + if (!Files.exists(path)) { + return; + } + try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + JsonObject obj = new Gson().fromJson(reader, JsonObject.class); + if (obj != null) { + loadPersistentData(obj); } } } catch (Exception e) { @@ -96,45 +158,17 @@ public void performLoadPersistentData() { } public abstract BuildableUI getBuildableUI(); + public abstract void savePersistentData(JsonObject obj); + public abstract void loadPersistentData(JsonObject obj); - @Override - public boolean onPreTick() { - player = LocalPlayer.self(); - return super.onPreTick() && player != null && player.isValid(); + protected void onItemAcquired(InventoryItem item) { } - @Override - public void onActivation() { - super.onActivation(); - if (STOPWATCH == null) { - STOPWATCH = Stopwatch.startNew(); - } else { - STOPWATCH.resume(); - } + protected void onItemRemoved(InventoryItem item) { } - @Override - public void onDeactivation() { - super.onDeactivation(); - STOPWATCH.pause(); + protected void onItemChange(InventoryItem previous, InventoryItem current) { } - - @EventInfo(type = InventoryEvent.class) - private void onInventoryEvent(InventoryEvent event) { - - // New Item Acquired - if (event.oldItem().getId() <= -1 && event.newItem().getId() > -1) { - onItemAcquired(event); - } else if (event.oldItem().getId() > -1 && event.newItem().getId() <= -1) { - onItemRemoved(event); - } else { - onItemChange(event); - } - } - - protected void onItemAcquired(InventoryEvent event) {}; - protected void onItemRemoved(InventoryEvent event) {}; - protected void onItemChange(InventoryEvent event) {}; } diff --git a/src/main/java/net/botwithus/xapi/script/base/DelayableScript.java b/src/main/java/net/botwithus/xapi/script/base/DelayableScript.java index f42d8cd..fbedbb0 100644 --- a/src/main/java/net/botwithus/xapi/script/base/DelayableScript.java +++ b/src/main/java/net/botwithus/xapi/script/base/DelayableScript.java @@ -1,69 +1,139 @@ package net.botwithus.xapi.script.base; -import net.botwithus.rs3.client.Client; -import net.botwithus.scripts.Script; -import net.botwithus.util.Rand; +import com.botwithus.bot.api.BotScript; +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.ScriptContext; +import net.botwithus.xapi.XApi; -import java.util.Arrays; +import java.util.Objects; import java.util.concurrent.Callable; +import java.util.concurrent.ThreadLocalRandom; -public abstract class DelayableScript extends Script { +public abstract class DelayableScript implements BotScript { - private Callable delayUntil = null, - delayWhile = null; - private int ticksToDelay = -1, previousTick, currentTick; + private ScriptContext context; + private Callable delayUntil; + private Callable delayWhile; + private long delayUntilAt; @Override - public void run() { - try { - currentTick = Client.getServerTick(); - if (currentTick <= previousTick) { - return; - } + public final void onStart(ScriptContext ctx) { + this.context = Objects.requireNonNull(ctx, "ctx"); + XApi.bind(ctx); + onInitialize(); + onActivation(); + } + @Override + public final int onLoop() { + XApi.bind(context); + try { if (delayUntil != null) { - if (delayUntil.call() || ticksToDelay <= 0) { + if (delayUntil.call() || expired()) { delayUntil = null; - ticksToDelay = 0; + delayUntilAt = 0L; } else { - ticksToDelay--; + return 50; } - } else if (delayWhile != null) { - if (!delayWhile.call() || ticksToDelay <= 0) { + } + + if (delayWhile != null) { + if (!delayWhile.call() || expired()) { delayWhile = null; - ticksToDelay = 0; + delayUntilAt = 0L; } else { - ticksToDelay--; + return 50; } } - if (ticksToDelay > 0) { - ticksToDelay--; - } else { - doRun(); + if (delayUntilAt > 0L && !expired()) { + return (int) Math.min(250L, Math.max(10L, delayUntilAt - System.currentTimeMillis())); } - previousTick = currentTick; + delayUntilAt = 0L; + doRun(); + return Math.max(10, defaultLoopDelayMs()); } catch (Exception e) { - println("Exception: " + e.getMessage() + "\n" + Arrays.toString(e.getStackTrace())); + println("Exception: " + e.getMessage()); e.printStackTrace(); + return Math.max(250, defaultLoopDelayMs()); } } - public abstract void doRun(); + @Override + public final void onStop() { + try { + onDeactivation(); + } finally { + try { + onShutdown(); + } finally { + XApi.clear(); + } + } + } public void delayUntil(Callable condition, int timeoutTicks) { - delayUntil = condition; - ticksToDelay = timeoutTicks; + this.delayUntil = condition; + this.delayWhile = null; + this.delayUntilAt = System.currentTimeMillis() + ticksToMillis(timeoutTicks); } + public void delayWhile(Callable condition, int timeoutTicks) { - delayWhile = condition; - ticksToDelay = timeoutTicks; + this.delayWhile = condition; + this.delayUntil = null; + this.delayUntilAt = System.currentTimeMillis() + ticksToMillis(timeoutTicks); } + public void delay(int ticks) { - ticksToDelay = ticks; + this.delayUntil = null; + this.delayWhile = null; + this.delayUntilAt = System.currentTimeMillis() + ticksToMillis(ticks); } + public void delay(int min, int max) { - ticksToDelay = Rand.nextInt(min, max); + delay(ThreadLocalRandom.current().nextInt(min, max + 1)); + } + + protected long ticksToMillis(int ticks) { + return Math.max(0L, ticks) * 600L; + } + + protected boolean expired() { + return delayUntilAt > 0L && System.currentTimeMillis() >= delayUntilAt; } + + protected int defaultLoopDelayMs() { + return 300; + } + + protected ScriptContext context() { + return context; + } + + protected GameAPI gameApi() { + return context.getGameAPI(); + } + + protected GameAPI gameApi(String clientName) { + return XApi.api(clientName); + } + + protected void onInitialize() { + } + + protected void onShutdown() { + } + + public void onActivation() { + } + + public void onDeactivation() { + } + + public void println(String message) { + System.out.println("[" + getClass().getSimpleName() + "] " + message); + } + + public abstract void doRun(); } diff --git a/src/main/java/net/botwithus/xapi/script/permissive/base/PermissiveScript.java b/src/main/java/net/botwithus/xapi/script/permissive/base/PermissiveScript.java index 8f422f7..4ddb05d 100644 --- a/src/main/java/net/botwithus/xapi/script/permissive/base/PermissiveScript.java +++ b/src/main/java/net/botwithus/xapi/script/permissive/base/PermissiveScript.java @@ -1,21 +1,16 @@ package net.botwithus.xapi.script.permissive.base; -import java.util.Arrays; -import java.util.Map; - import net.botwithus.scripts.Info; -import net.botwithus.ui.workspace.Workspace; import net.botwithus.xapi.script.base.DelayableScript; import net.botwithus.xapi.script.permissive.node.Branch; import net.botwithus.xapi.script.permissive.node.TreeNode; import net.botwithus.xapi.script.permissive.node.leaf.ChainedActionLeaf; -import net.botwithus.xapi.script.ui.BwuGraphicsContext; -import net.botwithus.xapi.script.ui.interfaces.BuildableUI; -import net.botwithus.xapi.util.time.Stopwatch; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Arrays; import java.util.HashMap; +import java.util.Map; public abstract class PermissiveScript extends DelayableScript { diff --git a/src/main/java/net/botwithus/xapi/script/permissive/node/Branch.java b/src/main/java/net/botwithus/xapi/script/permissive/node/Branch.java index a5d48a8..f464670 100644 --- a/src/main/java/net/botwithus/xapi/script/permissive/node/Branch.java +++ b/src/main/java/net/botwithus/xapi/script/permissive/node/Branch.java @@ -1,6 +1,5 @@ package net.botwithus.xapi.script.permissive.node; -import net.botwithus.scripts.Script; import net.botwithus.xapi.script.permissive.Interlock; import net.botwithus.xapi.script.permissive.base.PermissiveScript; import net.botwithus.xapi.script.permissive.serialization.InterlockJson; diff --git a/src/main/java/net/botwithus/xapi/script/permissive/node/LeafNode.java b/src/main/java/net/botwithus/xapi/script/permissive/node/LeafNode.java index f113d8a..cdc18a6 100644 --- a/src/main/java/net/botwithus/xapi/script/permissive/node/LeafNode.java +++ b/src/main/java/net/botwithus/xapi/script/permissive/node/LeafNode.java @@ -1,6 +1,5 @@ package net.botwithus.xapi.script.permissive.node; -import net.botwithus.scripts.Script; import net.botwithus.xapi.script.permissive.base.PermissiveScript; import java.util.concurrent.Callable; diff --git a/src/main/java/net/botwithus/xapi/script/permissive/node/TreeNode.java b/src/main/java/net/botwithus/xapi/script/permissive/node/TreeNode.java index 9505416..715e3c0 100644 --- a/src/main/java/net/botwithus/xapi/script/permissive/node/TreeNode.java +++ b/src/main/java/net/botwithus/xapi/script/permissive/node/TreeNode.java @@ -1,6 +1,5 @@ package net.botwithus.xapi.script.permissive.node; -import net.botwithus.scripts.Script; import net.botwithus.xapi.script.permissive.EvaluationResult; import net.botwithus.xapi.script.permissive.base.PermissiveScript; import net.botwithus.xapi.script.permissive.interfaces.ITreeNode; diff --git a/src/main/java/net/botwithus/xapi/script/permissive/node/leaf/InteractiveLeaf.java b/src/main/java/net/botwithus/xapi/script/permissive/node/leaf/InteractiveLeaf.java index 2742770..a92baaf 100644 --- a/src/main/java/net/botwithus/xapi/script/permissive/node/leaf/InteractiveLeaf.java +++ b/src/main/java/net/botwithus/xapi/script/permissive/node/leaf/InteractiveLeaf.java @@ -1,7 +1,6 @@ package net.botwithus.xapi.script.permissive.node.leaf; import net.botwithus.rs3.minimenu.Interactive; -import net.botwithus.scripts.Script; import net.botwithus.xapi.script.permissive.base.PermissiveScript; import net.botwithus.xapi.script.permissive.node.LeafNode; diff --git a/src/main/java/net/botwithus/xapi/script/task/AbstractTask.java b/src/main/java/net/botwithus/xapi/script/task/AbstractTask.java index e5f1b96..17ef65e 100644 --- a/src/main/java/net/botwithus/xapi/script/task/AbstractTask.java +++ b/src/main/java/net/botwithus/xapi/script/task/AbstractTask.java @@ -1,11 +1,9 @@ package net.botwithus.xapi.script.task; import com.google.gson.JsonObject; -import lombok.Getter; public abstract class AbstractTask implements Task { - @Getter private final T target; // Generic target, like Hotspot, Tree, Rock, etc. private int currentCount; @@ -28,6 +26,10 @@ public AbstractTask(T target, int currentCount, int completeCount) { this.isComplete = false; } + public T getTarget() { + return target; + } + @Override public boolean isComplete() { return isComplete; @@ -107,4 +109,4 @@ public void deserialize(JsonObject json) { JsonObject targetJson = json.getAsJsonObject("target"); this.target.deserialize(targetJson); } -} \ No newline at end of file +} diff --git a/src/main/java/net/botwithus/xapi/script/ui/BwuGraphicsContext.java b/src/main/java/net/botwithus/xapi/script/ui/BwuGraphicsContext.java index 6b6129c..16a1f4f 100644 --- a/src/main/java/net/botwithus/xapi/script/ui/BwuGraphicsContext.java +++ b/src/main/java/net/botwithus/xapi/script/ui/BwuGraphicsContext.java @@ -1,13 +1,8 @@ package net.botwithus.xapi.script.ui; -import net.botwithus.imgui.ImGui; +import net.botwithus.scripts.Info; import net.botwithus.ui.workspace.Workspace; import net.botwithus.xapi.script.BwuScript; -import net.botwithus.xapi.util.StringUtils; -import net.botwithus.xapi.util.collection.Pair; -import net.botwithus.xapi.util.statistic.XPInfo; -import net.botwithus.xapi.util.time.DurationStringFormat; -import net.botwithus.xapi.util.time.Timer; public class BwuGraphicsContext { private final BwuScript script; @@ -16,55 +11,14 @@ public class BwuGraphicsContext { public BwuGraphicsContext(BwuScript script, Workspace workspace) { this.script = script; this.workspace = workspace; - - this.workspace.setName(script.getInfo().name()); + Info info = script.getInfo(); + workspace.setName(info != null && !info.name().isBlank() ? info.name() : script.getName()); } public void draw() { - - if (ImGui.begin(script.getInfo().name() + " Settings | " + script.getInfo().version(), 0)) { -// ImGui.pushStyleColor(5, 0.322f, 0.494f,0.675f, 0.400f); -// ImGui.pushStyleColor(7, 0.322f, 0.494f,0.675f, 0.200f); -// ImGui.pushStyleColor(18, 0.322f, 0.494f,0.720f, 0.800f); -// ImGui.pushStyleColor(21, 0.322f, 0.494f,0.675f, 0.400f); - - if (ImGui.beginTabBar("Bot", 0)) { - if (ImGui.beginTabItem("Config", 0)) { - script.onDrawConfig(workspace); - ImGui.endTabItem(); - } - if (ImGui.beginTabItem("Stats", 0)) { - if (script.botStatInfo != null) { - if (script.botStatInfo.xpInfoMap != null && !script.botStatInfo.xpInfoMap.isEmpty()) { - for (var key : script.botStatInfo.xpInfoMap.keySet()) { - XPInfo model = script.botStatInfo.xpInfoMap.get(key); - var pairs = model.getPairList(script.STOPWATCH); - if (!pairs.isEmpty()) { - ImGui.separatorText(StringUtils.toTitleCase(model.getSkillsType().name())); - for (Pair infoUI : pairs) - ImGui.text(infoUI.getLeft() + infoUI.getRight()); - } - } - ImGui.separator(); - } - if (script.botStatInfo.displayInfoMap != null && !script.botStatInfo.displayInfoMap.isEmpty()) { - for (String key : script.botStatInfo.displayInfoMap.keySet()) { - ImGui.text(key + script.botStatInfo.displayInfoMap.get(key)); - } - ImGui.separator(); - } - - ImGui.text("Runtime: " + Timer.secondsToFormattedString(script.STOPWATCH.elapsed() / 1000, DurationStringFormat.CLOCK)); - ImGui.text("Current Task: " + script.getCurrentState().getStatus()); - } - ImGui.endTabItem(); - } - } - ImGui.endTabBar(); - -// ImGui.popStyleColor(4); + if (script.getBuildableUI() != null) { + script.getBuildableUI().buildUI(); } - ImGui.end(); + script.onDrawConfig(workspace); } - } diff --git a/src/main/java/net/botwithus/xapi/util/BwuDistance.java b/src/main/java/net/botwithus/xapi/util/BwuDistance.java index 6c4d7b1..5d0a2cb 100644 --- a/src/main/java/net/botwithus/xapi/util/BwuDistance.java +++ b/src/main/java/net/botwithus/xapi/util/BwuDistance.java @@ -1,26 +1,31 @@ package net.botwithus.xapi.util; -import net.botwithus.rs3.world.Area; -import net.botwithus.rs3.world.Distance; -import net.botwithus.rs3.world.Locatable; +import net.botwithus.xapi.util.position.Positionable; -public class BwuDistance { - public static boolean isLocatableCloser(Locatable primary, Locatable secondary, Locatable destination) { - var primaryToDestination = Distance.between(primary, destination); - var secondaryToDestination = Distance.between(secondary, destination); - return primaryToDestination >= secondaryToDestination; +public final class BwuDistance { + + private BwuDistance() { } - public static boolean isLocatableBetween(Locatable primary, Locatable secondary, Locatable destination) { - if (primary == null || secondary == null || destination == null || primary.getCoordinate() == null || secondary.getCoordinate() == null || destination.getCoordinate() == null) - return false; + public static boolean isLocatableCloser(Positionable primary, Positionable secondary, Positionable destination) { + return distance(primary, destination) >= distance(secondary, destination); + } - var center = new Area.Rectangular(primary.getCoordinate(), destination.getCoordinate()).getCentroid(); - if (center == null) + public static boolean isLocatableBetween(Positionable primary, Positionable secondary, Positionable destination) { + if (primary == null || secondary == null || destination == null) { return false; + } + int centerX = (primary.x() + destination.x()) / 2; + int centerY = (primary.y() + destination.y()) / 2; + return chebyshev(primary.x(), primary.y(), centerX, centerY) + >= chebyshev(secondary.x(), secondary.y(), centerX, centerY); + } + + public static int distance(Positionable a, Positionable b) { + return chebyshev(a.x(), a.y(), b.x(), b.y()); + } - var primaryToCenter = Distance.between(primary, center); - var secondaryToCenter = Distance.between(secondary, center); - return primaryToCenter >= secondaryToCenter; + public static int chebyshev(int x1, int y1, int x2, int y2) { + return Math.max(Math.abs(x1 - x2), Math.abs(y1 - y2)); } -} \ No newline at end of file +} diff --git a/src/main/java/net/botwithus/xapi/util/BwuTiming.java b/src/main/java/net/botwithus/xapi/util/BwuTiming.java new file mode 100644 index 0000000..01bcd7a --- /dev/null +++ b/src/main/java/net/botwithus/xapi/util/BwuTiming.java @@ -0,0 +1,64 @@ +package net.botwithus.xapi.util; + +import java.util.Random; +import java.util.function.BooleanSupplier; + +public final class BwuTiming { + + private static final Random RANDOM = new Random(); + private static final long TICK_MS = 600L; + + private BwuTiming() { + } + + public static void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public static void sleepRandom(long min, long max) { + sleep(min + (long) (RANDOM.nextDouble() * (max - min))); + } + + public static void sleepTick() { + sleep(TICK_MS); + } + + public static void shortDelay() { + sleepRandom(300, 600); + } + + public static void mediumDelay() { + sleepRandom(600, 1200); + } + + public static void longDelay() { + sleepRandom(1200, 2400); + } + + public static boolean waitUntil(BooleanSupplier condition, long timeoutMs) { + long end = System.currentTimeMillis() + timeoutMs; + while (System.currentTimeMillis() < end) { + if (condition.getAsBoolean()) { + return true; + } + sleep(50); + } + return condition.getAsBoolean(); + } + + public static boolean waitWhile(BooleanSupplier condition, long timeoutMs) { + return waitUntil(() -> !condition.getAsBoolean(), timeoutMs); + } + + public static int random(int min, int max) { + return min + RANDOM.nextInt(max - min + 1); + } + + public static long gaussianRandom(long mean, long stdDev) { + return Math.round(mean + RANDOM.nextGaussian() * stdDev); + } +} diff --git a/src/main/java/net/botwithus/xapi/util/Projection.java b/src/main/java/net/botwithus/xapi/util/Projection.java new file mode 100644 index 0000000..7971449 --- /dev/null +++ b/src/main/java/net/botwithus/xapi/util/Projection.java @@ -0,0 +1,29 @@ +package net.botwithus.xapi.util; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.model.ScreenPosition; +import net.botwithus.xapi.XApi; + +import java.util.List; + +public final class Projection { + + private Projection() { + } + + public static ScreenPosition worldToScreen(GameAPI api, int tileX, int tileY) { + return api.getWorldToScreen(tileX, tileY); + } + + public static ScreenPosition worldToScreen(int tileX, int tileY) { + return worldToScreen(XApi.api(), tileX, tileY); + } + + public static List batchWorldToScreen(GameAPI api, List coordinates) { + return api.batchWorldToScreen(coordinates); + } + + public static List batchWorldToScreen(List coordinates) { + return batchWorldToScreen(XApi.api(), coordinates); + } +} diff --git a/src/main/java/net/botwithus/xapi/util/Skills.java b/src/main/java/net/botwithus/xapi/util/Skills.java new file mode 100644 index 0000000..bba9859 --- /dev/null +++ b/src/main/java/net/botwithus/xapi/util/Skills.java @@ -0,0 +1,68 @@ +package net.botwithus.xapi.util; + +import com.botwithus.bot.api.GameAPI; +import com.botwithus.bot.api.model.PlayerStat; +import net.botwithus.xapi.XApi; + +public final class Skills { + + public static final int ATTACK = 0; + public static final int DEFENCE = 1; + public static final int STRENGTH = 2; + public static final int CONSTITUTION = 3; + public static final int RANGED = 4; + public static final int PRAYER = 5; + public static final int MAGIC = 6; + public static final int COOKING = 7; + public static final int WOODCUTTING = 8; + public static final int FLETCHING = 9; + public static final int FISHING = 10; + public static final int FIREMAKING = 11; + public static final int CRAFTING = 12; + public static final int SMITHING = 13; + public static final int MINING = 14; + public static final int HERBLORE = 15; + public static final int AGILITY = 16; + public static final int THIEVING = 17; + public static final int SLAYER = 18; + public static final int FARMING = 19; + public static final int RUNECRAFTING = 20; + public static final int HUNTER = 21; + public static final int CONSTRUCTION = 22; + public static final int SUMMONING = 23; + public static final int DUNGEONEERING = 24; + public static final int DIVINATION = 25; + public static final int INVENTION = 26; + public static final int ARCHAEOLOGY = 27; + public static final int NECROMANCY = 28; + + private Skills() { + } + + public static int getLevel(GameAPI api, int skillId) { + PlayerStat stat = api.getPlayerStat(skillId); + return stat == null ? 0 : stat.level(); + } + + public static int getLevel(int skillId) { + return getLevel(XApi.api(), skillId); + } + + public static int getBoostedLevel(GameAPI api, int skillId) { + PlayerStat stat = api.getPlayerStat(skillId); + return stat == null ? 0 : stat.boostedLevel(); + } + + public static int getBoostedLevel(int skillId) { + return getBoostedLevel(XApi.api(), skillId); + } + + public static int getXp(GameAPI api, int skillId) { + PlayerStat stat = api.getPlayerStat(skillId); + return stat == null ? 0 : stat.xp(); + } + + public static int getXp(int skillId) { + return getXp(XApi.api(), skillId); + } +} diff --git a/src/main/java/net/botwithus/xapi/util/position/Positionable.java b/src/main/java/net/botwithus/xapi/util/position/Positionable.java new file mode 100644 index 0000000..06511c1 --- /dev/null +++ b/src/main/java/net/botwithus/xapi/util/position/Positionable.java @@ -0,0 +1,7 @@ +package net.botwithus.xapi.util.position; + +public interface Positionable { + int x(); + int y(); + int plane(); +} diff --git a/src/main/java/net/botwithus/xapi/util/statistic/XPInfo.java b/src/main/java/net/botwithus/xapi/util/statistic/XPInfo.java index 1fa928d..d8ef717 100644 --- a/src/main/java/net/botwithus/xapi/util/statistic/XPInfo.java +++ b/src/main/java/net/botwithus/xapi/util/statistic/XPInfo.java @@ -1,38 +1,37 @@ package net.botwithus.xapi.util.statistic; -import net.botwithus.rs3.stats.Stats; import net.botwithus.xapi.util.BwuMath; -import net.botwithus.xapi.util.time.DurationStringFormat; -import net.botwithus.xapi.util.time.Stopwatch; import net.botwithus.xapi.util.StringUtils; import net.botwithus.xapi.util.collection.PairList; +import net.botwithus.xapi.util.time.DurationStringFormat; +import net.botwithus.xapi.util.time.Stopwatch; import net.botwithus.xapi.util.time.Timer; import java.text.NumberFormat; - public class XPInfo { - public Stats statType; - - private int startLvl, startXP, currentLvl, currentXP, xpUntilNextLevel; + private final String skillName; + private int startLvl; + private int startXP; + private int currentLvl; + private int currentXP; + private int xpUntilNextLevel; - public XPInfo(Stats stat) { - this.statType = stat; + public XPInfo(String skillName, int level, int xp, int xpUntilNextLevel) { + this.skillName = skillName; + snapshot(level, xp, xpUntilNextLevel); reset(); } - public void update() { - this.currentLvl = statType.getLevel(); - this.currentXP = statType.getXp(); - this.xpUntilNextLevel = statType.getStat().getXpUntilNextLevel(); + public void snapshot(int level, int xp, int xpUntilNextLevel) { + this.currentLvl = level; + this.currentXP = xp; + this.xpUntilNextLevel = xpUntilNextLevel; } public void reset() { - this.currentLvl = statType.getLevel(); - this.currentXP = statType.getXp(); - this.xpUntilNextLevel = statType.getStat().getXpUntilNextLevel(); - currentLvl = startLvl; - currentXP = startXP; + this.startLvl = currentLvl; + this.startXP = currentXP; } public int getLevelsGained() { @@ -48,13 +47,17 @@ public int getXPHour(Stopwatch watch) { } public int getSecondsUntilLevel(Stopwatch watch) { - return (int) ((((double) xpUntilNextLevel) / ((double) getXPHour(watch))) * 3600.0); + int rate = getXPHour(watch); + if (rate <= 0) { + return Integer.MAX_VALUE; + } + return (int) ((((double) xpUntilNextLevel) / rate) * 3600.0); } public PairList getPairList(Stopwatch stopWatch) { PairList list = new PairList<>(); if (currentXP > startXP) { - var name = StringUtils.toTitleCase(statType.toString()); + String name = StringUtils.toTitleCase(skillName); list.add(name + " Level: ", currentLvl + " (" + getLevelsGained() + " Gained)"); list.add(name + " XP Gained: ", NumberFormat.getIntegerInstance().format(getGainedXP()) + " (" + NumberFormat.getIntegerInstance().format(getXPHour(stopWatch)) + "/Hour)"); list.add(name + " TTL: ", Timer.secondsToFormattedString(getSecondsUntilLevel(stopWatch), DurationStringFormat.DESCRIPTION)); @@ -62,7 +65,7 @@ public PairList getPairList(Stopwatch stopWatch) { return list; } - public Stats getSkillsType() { - return statType; + public String getSkillsType() { + return skillName; } -} \ No newline at end of file +} diff --git a/src/main/java/net/botwithus/xapi/util/time/Timer.java b/src/main/java/net/botwithus/xapi/util/time/Timer.java index ea35a63..090322f 100644 --- a/src/main/java/net/botwithus/xapi/util/time/Timer.java +++ b/src/main/java/net/botwithus/xapi/util/time/Timer.java @@ -1,13 +1,9 @@ package net.botwithus.xapi.util.time; -import lombok.Getter; -import lombok.Setter; -import net.botwithus.util.Rand; +import java.util.concurrent.ThreadLocalRandom; public class Timer { private final Stopwatch stopWatch = new Stopwatch(); - @Setter - @Getter private long minTime, maxTime; private boolean hasStarted = false, forceExpired = false; private long timerDuration; @@ -15,7 +11,7 @@ public class Timer { public Timer(long min, long max) { minTime = min; maxTime = max; - timerDuration = Rand.nextLong(min, max); + timerDuration = nextDuration(); forceExpired = false; } @@ -54,7 +50,7 @@ public void setRemainingTime(long time) { public void reset() { forceExpired = false; hasStarted = true; - timerDuration = Rand.nextLong(minTime, maxTime); + timerDuration = nextDuration(); stopWatch.start(); } @@ -78,6 +74,31 @@ public boolean hasStarted() { return hasStarted; } + public long getMinTime() { + return minTime; + } + + public void setMinTime(long minTime) { + this.minTime = minTime; + } + + public long getMaxTime() { + return maxTime; + } + + public void setMaxTime(long maxTime) { + this.maxTime = maxTime; + } + + private long nextDuration() { + if (minTime == maxTime) { + return minTime; + } + long lower = Math.min(minTime, maxTime); + long upper = Math.max(minTime, maxTime); + return ThreadLocalRandom.current().nextLong(lower, upper + 1); + } + public static String secondsToFormattedString(long timeInSeconds, DurationStringFormat stringFormat) { long days = timeInSeconds / 86400, hours = (timeInSeconds % 86400) / 3600, minutes = ((timeInSeconds % 86400) % 3600) / 60, seconds = timeInSeconds % 60; @@ -108,4 +129,4 @@ private static String getClockFormat(long number) { return Long.toString(number); } } -} \ No newline at end of file +}