diff --git a/build.gradle.kts b/build.gradle.kts index d3ae698..c2653d3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation("net.botwithus.api:api:1.+") implementation("net.botwithus.imgui:imgui:1.+") implementation("org.projectlombok:lombok:1.18.26") + implementation("botwithus.navigation:nav-api:1.0.1-SNAPSHOT") implementation("com.google.code.gson:gson:2.10.1") // Logging dependencies diff --git a/src/main/java/net/botwithus/xapi/game/inventory/Bank.java b/src/main/java/net/botwithus/xapi/game/inventory/Bank.java index 77bc4c0..6588c95 100644 --- a/src/main/java/net/botwithus/xapi/game/inventory/Bank.java +++ b/src/main/java/net/botwithus/xapi/game/inventory/Bank.java @@ -42,58 +42,54 @@ public class Bank { */ public static boolean open() { try { - logger.info("Attempting find bank obj"); - var obj = SceneObjectQuery.newQuery().name(BANK_NAME_PATTERN).option("Use") + logger.debug("Starting bank open attempt"); + var objectQuery = 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(); + .or(SceneObjectQuery.newQuery().name("Shantay chest")); + var obj = objectQuery.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")); + var npcQuery = NpcQuery.newQuery().option("Bank"); + var npc = npcQuery.results().nearest(); + logger.debug("Bank candidates resolved: objectName={}, npcName={}", + obj != null ? obj.getName() : "none", + npc != null ? npc.getName() : "none"); + var useObj = true; 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); + logger.debug("Candidate distances -> object: {}, npc: {}", objDist, npcDist); + if (!Double.isNaN(objDist) && !Double.isNaN(npcDist)) { + useObj = objDist < npcDist; + } + logger.debug("Interaction source resolved to {}", useObj ? "object" : "npc"); } + 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; - } + logger.debug("Attempting object interaction -> name={}, options={}", obj.getName(), actions); + var action = actions.stream().filter(option -> option != null && !option.isEmpty()).findFirst(); + if (action.isPresent()) { + var option = action.get(); + logger.debug("Invoking object action {}", option); + var interactionResult = obj.interact(option); + logger.debug("Object interaction result={}", interactionResult); + return interactionResult > 0; } else { - logger.warn("No options available on bank object"); + logger.warn("No valid action found for bank object {}", obj.getName()); return false; } } else if (npc != null) { - logger.info("Interacting via NPC"); + logger.debug("Attempting NPC interaction -> name={}", npc.getName()); var interactionResult = npc.interact("Bank"); - logger.info("NPC interaction completed: " + interactionResult); + logger.debug("NPC interaction result={}", interactionResult); return interactionResult > 0; } - logger.warn("No valid bank object or NPC found"); + + logger.warn("No valid bank object or NPC found during open attempt"); return false; } catch (Exception e) { - logger.error(e.getMessage(), e); + logger.error("Unexpected error while opening bank", e); return false; } } @@ -103,7 +99,9 @@ public static boolean open() { * @return true if bank is open, false otherwise */ public static boolean isOpen() { - return Interfaces.isOpen(INTERFACE_INDEX); + var open = Interfaces.isOpen(INTERFACE_INDEX); + logger.debug("Bank interface open state -> {}", open); + return open; } /** @@ -112,7 +110,10 @@ public static boolean isOpen() { * @return true if the interface was closed, false otherwise */ public static boolean close() { - return MiniMenu.doAction(Action.COMPONENT, 1, -1, 33882430) > 0; + logger.debug("Close bank request"); + var result = MiniMenu.doAction(Action.COMPONENT, 1, -1, 33882430) > 0; + logger.debug("Close bank result -> success={}", result); + return result; } /** @@ -125,30 +126,36 @@ public static Inventory getInventory() { } public static boolean loadLastPreset() { + logger.debug("Load last preset request"); 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")); + logger.debug("Last preset candidates -> object={}, npc={}", + obj != null ? obj.getName() : "none", + npc != null ? npc.getName() : "none"); + var useObj = true; 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); + logger.debug("Last preset distances -> object: {}, npc: {}", objDist, npcDist); + if (!Double.isNaN(objDist) && !Double.isNaN(npcDist)) { + useObj = objDist < npcDist; + } + logger.debug("Last preset interaction source -> {}", useObj ? "object" : "npc"); } if (obj != null && useObj) { -// logger.debug("Interacting via Object: " + obj.getName()); - return obj.interact(LAST_PRESET_OPTION) > 0; + logger.debug("Interacting with preset object {}", obj.getName()); + var result = obj.interact(LAST_PRESET_OPTION); + logger.debug("Preset object interaction result={}", result); + return result > 0; } else if (npc != null) { -// logger.debug("Interacting via Npc: " + npc.getName()); - return npc.interact(LAST_PRESET_OPTION) > 0; + logger.debug("Interacting with preset NPC {}", npc.getName()); + var result = npc.interact(LAST_PRESET_OPTION); + logger.debug("Preset NPC interaction result={}", result); + return result > 0; } + logger.warn("No valid object or NPC found for last preset request"); return false; } @@ -191,16 +198,30 @@ public static boolean isEmpty() { } public static boolean interact(int slot, int option) { + logger.debug("Bank component interact request -> slot={}, option={}", slot, 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; + if (item == null) { + logger.debug("Bank component interact aborted -> no item at slot {}", slot); + return false; } - return false; + + logger.debug("Bank component interact matched itemId={}, name={}, slot={}", item.getId(), item.getName(), item.getSlot()); + ResultSet queryResults = ComponentQuery.newQuery(INTERFACE_INDEX) + .id(COMPONENT_INDEX) + .itemId(item.getId()) + .results(); + + logger.debug("Bank component interact found {} component candidates", queryResults.size()); + var component = queryResults.first(); + if (component == null) { + logger.warn("No bank component found for slot {} itemId {}", slot, item.getId()); + return false; + } + + var interactionResult = component.interact(option); + logger.debug("Bank component interact result={} for componentId={}", interactionResult, component.getItemId()); + return interactionResult > 0; } /** @@ -236,14 +257,18 @@ public static int getCount(Pattern namePattern) { * @param option the doAction option to execute on the item. */ public static boolean withdraw(InventoryItemQuery query, int option) { + logger.debug("Withdraw request -> option={}", option); setTransferOption(TransferOptionType.ALL); var item = query.results().first(); - if (item != null) { - logger.info("Item: " + item.getName()); - } else { - logger.info("Item is null"); + if (item == null) { + logger.debug("Withdraw request failed -> no matching item"); + return false; } - return item != null && interact(item.getSlot(), option); + + logger.debug("Withdraw executing -> itemId={}, name={}, slot={}", item.getId(), item.getName(), item.getSlot()); + var success = interact(item.getSlot(), option); + logger.debug("Withdraw result -> success={}, option={}, itemId={}", success, option, item.getId()); + return success; } /** @@ -254,10 +279,12 @@ public static boolean withdraw(InventoryItemQuery query, int option) { * @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); + logger.debug("Withdraw by name request -> name={}, option={}", itemName, option); + if (itemName == null || itemName.isEmpty()) { + logger.debug("Withdraw by name aborted -> name is empty"); + return false; } - return false; + return withdraw(InventoryItemQuery.newQuery(INVENTORY_ID).name(itemName), option); } /** @@ -268,10 +295,12 @@ public static boolean withdraw(String itemName, int option) { * @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); + logger.debug("Withdraw by id request -> id={}, option={}", itemId, option); + if (itemId < 0) { + logger.debug("Withdraw by id aborted -> id is negative"); + return false; } - return false; + return withdraw(InventoryItemQuery.newQuery(INVENTORY_ID).id(itemId), option); } /** @@ -282,10 +311,12 @@ public static boolean withdraw(int itemId, int option) { * @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); + logger.debug("Withdraw by pattern request -> option={}, pattern={}", option, pattern); + if (pattern == null) { + logger.debug("Withdraw by pattern aborted -> pattern is null"); + return false; } - return false; + return withdraw(InventoryItemQuery.newQuery(INVENTORY_ID).name(pattern), option); } /** @@ -295,14 +326,17 @@ public static boolean withdraw(Pattern pattern, int option) { * @return true if the item was successfully withdrawn, false otherwise. */ public static boolean withdrawAll(String name) { + logger.debug("Withdraw-all by name request -> name={}", name); return withdraw(InventoryItemQuery.newQuery(INVENTORY_ID).name(name), 1); } public static boolean withdrawAll(int id) { + logger.debug("Withdraw-all by id request -> id={}", id); return withdraw(InventoryItemQuery.newQuery(INVENTORY_ID).id(id), 1); } public static boolean withdrawAll(Pattern pattern) { + logger.debug("Withdraw-all by pattern request -> pattern={}", pattern); return withdraw(InventoryItemQuery.newQuery(INVENTORY_ID).name(pattern), 1); } @@ -312,9 +346,19 @@ public static boolean withdrawAll(Pattern pattern) { * @return true if the items were successfully deposited, false otherwise */ public static boolean depositAll() { + logger.debug("Deposit all carried items request"); setTransferOption(TransferOptionType.ALL); - var comp = ComponentQuery.newQuery(INTERFACE_INDEX).option("Deposit carried items").results().first(); - return comp != null && comp.interact(1) > 0; + var comp = ComponentQuery.newQuery(INTERFACE_INDEX) + .option("Deposit carried items") + .results() + .first(); + if (comp == null) { + logger.warn("Deposit all component not found"); + return false; + } + var result = comp.interact(1); + logger.debug("Deposit all interaction result={}", result); + return result > 0; } /** @@ -323,8 +367,15 @@ public static boolean depositAll() { * @return true if the items were successfully deposited, false otherwise */ public static boolean depositEquipment() { + logger.debug("Deposit equipment request"); Component component = ComponentQuery.newQuery(INTERFACE_INDEX).id(42).results().first(); - return component != null && component.interact(1) > 0; + if (component == null) { + logger.warn("Deposit equipment component not found"); + return false; + } + var result = component.interact(1); + logger.debug("Deposit equipment interaction result={}", result); + return result > 0; } /** @@ -333,8 +384,15 @@ public static boolean depositEquipment() { * @return true if the items were successfully deposited, false otherwise */ public static boolean depositBackpack() { + logger.debug("Deposit backpack request"); Component component = ComponentQuery.newQuery(INTERFACE_INDEX).id(39).results().first(); - return component != null && component.interact(1) > 0; + if (component == null) { + logger.warn("Deposit backpack component not found"); + return false; + } + var result = component.interact(1); + logger.debug("Deposit backpack interaction result={}", result); + return result > 0; } @@ -347,38 +405,85 @@ public static boolean depositBackpack() { * @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); + logger.debug("Deposit via component query -> option={}", option); + var component = query.results().first(); + if (component == null) { + logger.debug("Deposit via component query aborted -> no component match"); + return false; + } + logger.debug("Deposit via component query using componentId={} itemId={}", component.getItemId(), component.getItemId()); + return deposit(script, component, 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); + logger.debug("Deposit-all via component query request"); + var component = query.results().first(); + if (component == null) { + logger.debug("Deposit-all via component query aborted -> no component match"); + return false; + } + return deposit(script, component, 1); } public static boolean deposit(PermissiveScript script, Component comp, int option) { + logger.debug("Deposit component request -> option={}", option); setTransferOption(TransferOptionType.ALL); - var val = comp != null && comp.interact(option) > 0; - if (val) script.delay(Rand.nextInt(1, 2)); - return val; + if (comp == null) { + logger.warn("Deposit component request failed -> component not found"); + return false; + } + var interactionResult = comp.interact(option); + logger.debug("Deposit component interaction result={} for componentId={}", interactionResult, comp.getItemId()); + if (interactionResult > 0) { + script.delay(Rand.nextInt(1, 2)); + return true; + } + return false; } 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); + var namesDescription = itemNames == null ? "null" : Arrays.toString(itemNames); + var ids = InventoryItemQuery.newQuery(93).name(itemNames).results().stream() + .map(Item::getId) + .distinct() + .toList(); + logger.debug("Deposit-all by names -> names={}, distinctIds={}", namesDescription, ids.size()); + var results = ids.stream() + .map(id -> depositAll(script, ComponentQuery.newQuery(517).itemId(id))) + .toList(); + var success = !results.contains(false); + logger.debug("Deposit-all by names result -> success={}", success); + return success; } 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); + var idsDescription = Arrays.toString(itemIds); + var ids = InventoryItemQuery.newQuery(93).id(itemIds).results().stream() + .map(Item::getId) + .distinct() + .toList(); + logger.debug("Deposit-all by ids -> requestIds={}, distinctMatches={}", idsDescription, ids.size()); + var results = ids.stream() + .map(id -> depositAll(script, ComponentQuery.newQuery(517).itemId(id))) + .toList(); + var success = !results.contains(false); + logger.debug("Deposit-all by ids result -> success={}", success); + return success; } 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); + var patternDescription = patterns == null ? "null" : Arrays.toString(patterns); + var ids = InventoryItemQuery.newQuery(93).name(patterns).results().stream() + .map(Item::getId) + .distinct() + .toList(); + logger.debug("Deposit-all by patterns -> patterns={}, distinctMatches={}", patternDescription, ids.size()); + var results = ids.stream() + .map(id -> depositAll(script, ComponentQuery.newQuery(517).itemId(id))) + .toList(); + var success = !results.contains(false); + logger.debug("Deposit-all by patterns result -> success={}", success); + return success; } public static boolean depositAllExcept(PermissiveScript script, String... itemNames) { @@ -394,7 +499,11 @@ public static boolean depositAllExcept(PermissiveScript script, String... itemNa 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); + logger.debug("Deposit-all-except by names -> protectedNames={}, protectedIds={}, candidates={}", protectedNames.size(), protectedIds.size(), items.size()); + var results = items.stream().map(id -> depositAll(script, ComponentQuery.newQuery(517).itemId(id))).toList(); + var success = !results.contains(false); + logger.debug("Deposit-all-except by names result -> success={}", success); + return success; } public static boolean depositAllExcept(PermissiveScript script, int... ids) { @@ -403,7 +512,11 @@ public static boolean depositAllExcept(PermissiveScript script, int... ids) { 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); + logger.debug("Deposit-all-except by ids -> protectedIds={}, candidates={}", idSet.size(), items.size()); + var results = items.stream().map(i -> depositAll(script, ComponentQuery.newQuery(517).itemId(i))).toList(); + var success = !results.contains(false); + logger.debug("Deposit-all-except by ids result -> success={}", success); + return success; } public static boolean depositAllExcept(PermissiveScript script, Pattern... patterns) { @@ -413,7 +526,11 @@ public static boolean depositAllExcept(PermissiveScript script, Pattern... patte 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); + logger.debug("Deposit-all-except by patterns -> protectedIds={}, candidates={}", idMap.size(), items.size()); + var results = items.stream().map(i -> depositAll(script, ComponentQuery.newQuery(517).itemId(i))).toList(); + var success = !results.contains(false); + logger.debug("Deposit-all-except by patterns result -> success={}", success); + return success; } /** @@ -424,6 +541,7 @@ public static boolean depositAllExcept(PermissiveScript script, Pattern... patte * @return True if the item was successfully deposited, false otherwise. */ public static boolean deposit(PermissiveScript script, int itemId, int option) { + logger.debug("Deposit by id request -> itemId={}, option={}", itemId, option); return deposit(script, ComponentQuery.newQuery(517).itemId(itemId), option); } @@ -436,6 +554,7 @@ public static boolean deposit(PermissiveScript script, int itemId, int option) { * @return True if the item was successfully deposited, false otherwise. */ public static boolean deposit(PermissiveScript script, String name, BiFunction spred, int option) { + logger.debug("Deposit by name request -> name={}, option={}", name, option); return deposit(script, ComponentQuery.newQuery(517).itemName(name, spred), option); } @@ -447,6 +566,7 @@ public static boolean deposit(PermissiveScript script, String name, BiFunction name={}, option={}", name, option); return deposit(script, name, String::contentEquals, option); } @@ -459,14 +579,23 @@ public static boolean deposit(PermissiveScript script, String name, int option) */ // TODO: Update to no longer use MiniMenu.doAction public static boolean loadPreset(PermissiveScript script, int presetNumber) { + logger.debug("Load preset request -> presetNumber={}", presetNumber); int presetBrowsingValue = VarDomain.getVarBitValue(PRESET_BROWSING_VARBIT_ID); - if ((presetNumber >= 10 && presetBrowsingValue < 1) || (presetNumber < 10 && presetBrowsingValue > 0)) { + logger.debug("Preset browsing state -> value={}", presetBrowsingValue); + var requiresToggle = (presetNumber >= 10 && presetBrowsingValue < 1) || (presetNumber < 10 && presetBrowsingValue > 0); + if (requiresToggle) { + logger.debug("Adjusting preset browsing tab for preset {}", presetNumber); MiniMenu.doAction(Action.COMPONENT, 1, 100, 33882231); script.delay(Rand.nextInt(1, 2)); } - var result = MiniMenu.doAction(Action.COMPONENT, 1, presetNumber % 9,33882231) > 0; + var index = presetNumber % 9; + var result = MiniMenu.doAction(Action.COMPONENT, 1, index, 33882231) > 0; + logger.debug("Preset load interaction result -> success={}, index={}", result, index); if (result) { previousLoadedPreset = presetNumber; + logger.debug("Recorded previous loaded preset={}", previousLoadedPreset); + } else { + logger.warn("Failed to load preset {}", presetNumber); } return result; } @@ -479,17 +608,28 @@ public static boolean loadPreset(PermissiveScript script, int presetNumber) { * @return The value of the varbit. */ public static int getVarbitValue(int slot, int varbitId) { + logger.debug("Get varbit value request -> slot={}, varbitId={}", slot, varbitId); Inventory inventory = getInventory(); if (inventory == null) { + logger.warn("Bank inventory unavailable when retrieving varbit {}", varbitId); return Integer.MIN_VALUE; } - return inventory.getVarbitValue(slot, varbitId); + var value = inventory.getVarbitValue(slot, varbitId); + logger.debug("Get varbit value result -> value={}", value); + return value; } 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; + logger.debug("Transfer option request -> current={}, target={}", depositOptionState, transferoptionType); + if (depositOptionState == transferoptionType.getVarbitStateValue()) { + logger.debug("Transfer option already configured -> {}", transferoptionType); + return true; + } + var result = MiniMenu.doAction(Action.COMPONENT, 1, -1, 33882215) > 0; + logger.debug("Transfer option change result -> success={}, target={}", result, transferoptionType); + return result; } public static int getPreviousLoadedPreset() { @@ -513,4 +653,4 @@ enum TransferOptionType { public int getVarbitStateValue() { return varbitStateValue; } -} \ No newline at end of file +} 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..cd50c63 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,115 @@ 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 botwithus.navigation.api.NavPath; +import botwithus.navigation.api.State; +import botwithus.navigation.api.TeleportData; 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; -public class Traverse { - private final static Logger logger = LoggerFactory.getLogger(Traverse.class); +public final class Traverse { + private static final Logger logger = LoggerFactory.getLogger(Traverse.class); - private static final int MAX_LOCAL_DISTANCE = 80; - private static final int MAX_STEP_SIZE = 16; - private static final int MIN_STEP_SIZE = 10; + private Traverse() { + // Utility class + } /** - * 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 + * Resolve and process a navigation path using the BotWithUs navigation API. + * + * @param destinationCoord destination tile to reach. + * @return {@code true} if navigation could be started or destination already reached. */ 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); + return navigate(destinationCoord); } - + /** - * 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 + * Backwards compatible entry point that now delegates to the navigation API. + * The {@code minimap} and {@code stepSize} hints are ignored because the nav-api + * is responsible for optimising the route. */ 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"); - return false; - } - - 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); - } + return navigate(destinationCoord); } /** - * 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 + * Backwards compatible entry point that now delegates to the navigation API. + * The {@code minimap} hint is ignored because the nav-api determines the optimal route. */ public static boolean walkTo(Coordinate destinationCoord, boolean minimap) { + return navigate(destinationCoord); + } + + private static boolean navigate(Coordinate destinationCoord) { if (destinationCoord == null) { - logger.warn("ERROR: Coordinate is null"); + logger.warn("Cannot traverse: destination coordinate is null"); return false; } 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"); + if (Distance.to(destinationCoord) < 1.5d) { + logger.debug("Already adjacent to {}, skipping navigation.", destinationCoord); return true; } + } catch (Exception distanceError) { + logger.trace("Distance check failed before navigation: {}", distanceError.getMessage(), distanceError); + } - 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)); + try { + NavPath path = NavPath.resolve(destinationCoord); + if (path == null) { + logger.warn("Nav API returned no path for {}", destinationCoord); + return false; } - int result = MiniMenu.doAction(Action.WALK, minimap ? 1 : 0, destinationCoord.x(), destinationCoord.y()); + path.process(); + State state = path.state(); - 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; + path.getUsedTeleport().ifPresent(Traverse::logTeleportUsage); + + switch (state) { + case FINISHED -> { + logger.debug("Navigator reports destination {} already reached.", destinationCoord); + return true; + } + case CONTINUE -> { + logger.debug("Navigator processing path to {} (cost {}).", destinationCoord, path.getCost()); + return true; + } + case NO_PATH -> { + logger.warn("Navigator could not resolve a path to {}.", destinationCoord); + return false; + } + case FAILED -> { + logger.warn("Navigator failed while processing path to {}.", destinationCoord); + return false; + } + case IDLE -> { + logger.debug("Navigator is idle for destination {}.", destinationCoord); + return true; + } + default -> { + logger.warn("Navigator returned unexpected state {} for {}.", state, destinationCoord); + return false; + } } + } catch (UnsatisfiedLinkError e) { + logger.error("Nav API native bindings are unavailable while traversing to {}.", destinationCoord, e); + return false; } catch (Exception e) { - logger.trace("Exception while walking to " + destinationCoord.x() + ", " + destinationCoord.y() + ": " + e.getMessage(), e); + logger.error("Unexpected error while traversing to {}.", destinationCoord, e); return false; } } + + private static void logTeleportUsage(TeleportData teleportData) { + logger.info( + "Nav API selected teleport '{}' (type {}) to {}", + teleportData.optionName(), + teleportData.interactionType(), + teleportData.toTile() + ); + } } diff --git a/src/main/java/net/botwithus/xapi/query/ComponentQuery.java b/src/main/java/net/botwithus/xapi/query/ComponentQuery.java index bd93786..949f2c3 100644 --- a/src/main/java/net/botwithus/xapi/query/ComponentQuery.java +++ b/src/main/java/net/botwithus/xapi/query/ComponentQuery.java @@ -6,8 +6,10 @@ import net.botwithus.rs3.interfaces.InterfaceManager; import net.botwithus.rs3.interfaces.Interfaces; import net.botwithus.xapi.query.base.Query; +import net.botwithus.xapi.query.base.QueryCache; import net.botwithus.xapi.query.result.ResultSet; +import java.time.Duration; import java.util.Arrays; import java.util.Iterator; import java.util.Objects; @@ -17,6 +19,7 @@ public class ComponentQuery implements Query> { protected Predicate root; + private final QueryCache> cache = new QueryCache<>(); private int[] ids; /** @@ -39,6 +42,23 @@ public static ComponentQuery newQuery(int... ids) { return new ComponentQuery(ids); } + public ComponentQuery withCache(Duration ttl) { + cache.configure(ttl); + return this; + } + + public static ComponentQuery visible(int... interfaceIds) { + return newQuery(interfaceIds).hidden(false); + } + + public static ComponentQuery buttonWithText(int interfaceId, int componentId, String... text) { + return newQuery(interfaceId) + .id(componentId) + .hidden(false) + .type(ComponentType.BUTTON) + .text((needle, haystack) -> haystack != null && needle.equalsIgnoreCase(haystack.toString()), text); + } + /** * Filters components by type. * @@ -46,6 +66,8 @@ public static ComponentQuery newQuery(int... ids) { * @return the updated ComponentQuery */ public ComponentQuery type(ComponentType... type) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(type).anyMatch(i -> t.getType() == i)); return this; } @@ -57,6 +79,8 @@ public ComponentQuery type(ComponentType... type) { * @return the updated ComponentQuery */ public ComponentQuery id(int... ids) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(ids).anyMatch(i -> i == t.getComponentId())); return this; } @@ -68,6 +92,8 @@ public ComponentQuery id(int... ids) { * @return the updated ComponentQuery */ public ComponentQuery subComponentId(int... ids) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(ids).anyMatch(i -> i == t.getSubComponentId())); return this; } @@ -79,6 +105,8 @@ public ComponentQuery subComponentId(int... ids) { * @return the updated ComponentQuery */ public ComponentQuery hidden(boolean hidden) { + invalidateCache(); + this.root = this.root.and(t -> t.isHidden() == hidden); return this; } @@ -90,6 +118,8 @@ public ComponentQuery hidden(boolean hidden) { * @return the updated ComponentQuery */ public ComponentQuery properties(int... properties) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(properties).anyMatch(i -> i == t.getProperties())); return this; } @@ -101,6 +131,8 @@ public ComponentQuery properties(int... properties) { * @return the updated ComponentQuery */ public ComponentQuery fontId(int... fontIds) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(fontIds).anyMatch(i -> i == t.getFontId())); return this; } @@ -112,6 +144,8 @@ public ComponentQuery fontId(int... fontIds) { * @return the updated ComponentQuery */ public ComponentQuery color(int... colors) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(colors).anyMatch(i -> i == t.getColor())); return this; } @@ -123,6 +157,8 @@ public ComponentQuery color(int... colors) { * @return the updated ComponentQuery */ public ComponentQuery alpha(int... alphas) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(alphas).anyMatch(i -> i == t.getAlpha())); return this; } @@ -134,6 +170,8 @@ public ComponentQuery alpha(int... alphas) { * @return the updated ComponentQuery */ public ComponentQuery itemId(int... itemIds) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(itemIds).anyMatch(i -> i == t.getItemId())); return this; } @@ -146,6 +184,8 @@ public ComponentQuery itemId(int... itemIds) { * @return the updated ComponentQuery */ public ComponentQuery itemName(String name, BiFunction spred) { + invalidateCache(); + this.root = this.root.and(t -> { var itemName = ConfigManager.getItemProvider().provide(t.getItemId()).getName(); return spred.apply(name, itemName); @@ -170,6 +210,8 @@ public ComponentQuery itemName(String name) { * @return the updated ComponentQuery */ public ComponentQuery itemAmount(int... amounts) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(amounts).anyMatch(i -> i == t.getItemAmount())); return this; } @@ -181,6 +223,8 @@ public ComponentQuery itemAmount(int... amounts) { * @return the updated ComponentQuery */ public ComponentQuery spriteId(int... spriteIds) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(spriteIds).anyMatch(i -> i == t.getSpriteId())); return this; } @@ -193,6 +237,8 @@ public ComponentQuery spriteId(int... spriteIds) { * @return the updated ComponentQuery */ public ComponentQuery text(BiFunction spred, String... text) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(text).anyMatch(i -> spred.apply(i, t.getText()))); return this; } @@ -205,6 +251,8 @@ public ComponentQuery text(BiFunction spred, Stri * @return the updated ComponentQuery */ public ComponentQuery optionBasedText(BiFunction spred, String... text) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(text).anyMatch(i -> spred.apply(i, t.getOptionBase()))); return this; } @@ -217,6 +265,8 @@ public ComponentQuery optionBasedText(BiFunction * @return the updated ComponentQuery */ public ComponentQuery option(BiFunction spred, String... option) { + invalidateCache(); + 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))); @@ -241,6 +291,8 @@ public ComponentQuery option(String... option) { * @return the updated ComponentQuery */ public ComponentQuery params(int... params) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(params).anyMatch(i -> t.getParams().containsKey(i))); return this; } @@ -252,6 +304,8 @@ public ComponentQuery params(int... params) { * @return the updated ComponentQuery */ public ComponentQuery children(int... ids) { + invalidateCache(); + this.root = this.root.and(t -> Arrays.stream(ids).anyMatch(i -> t.getChildren().stream().anyMatch(j -> j.getComponentId() == i))); return this; } @@ -263,14 +317,14 @@ public ComponentQuery children(int... ids) { */ @Override public ResultSet results() { - return new ResultSet<>( + return cache.getOrCompute(() -> 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 - ); + )); } /** @@ -294,4 +348,8 @@ public boolean test(Component comp) { return this.root.test(comp); } + private void invalidateCache() { + cache.invalidate(); + } + } \ No newline at end of file diff --git a/src/main/java/net/botwithus/xapi/query/GroundItemQuery.java b/src/main/java/net/botwithus/xapi/query/GroundItemQuery.java index 48c0aa8..9c9d131 100644 --- a/src/main/java/net/botwithus/xapi/query/GroundItemQuery.java +++ b/src/main/java/net/botwithus/xapi/query/GroundItemQuery.java @@ -8,9 +8,12 @@ import net.botwithus.rs3.world.Distance; import net.botwithus.rs3.world.World; import net.botwithus.xapi.query.base.Query; +import net.botwithus.xapi.query.base.QueryCache; +import net.botwithus.xapi.query.result.GroundItemResultSet; import net.botwithus.xapi.query.result.ResultSet; import org.jetbrains.annotations.NotNull; +import java.time.Duration; import java.util.Arrays; import java.util.Iterator; import java.util.function.BiFunction; @@ -22,6 +25,7 @@ public class GroundItemQuery implements Query> { protected Predicate root; + private final QueryCache cache = new QueryCache<>(); /** * Constructs a new GroundItemQuery with a default predicate. @@ -40,17 +44,34 @@ public static GroundItemQuery newQuery() { return new GroundItemQuery(); } + public GroundItemQuery withCache(Duration ttl) { + cache.configure(ttl); + return this; + } + + public static GroundItemQuery lootable(int... ids) { + return newQuery().valid(true).id(ids); + } + + public static GroundItemQuery lootableWithin(double distance, int... ids) { + return lootable(ids).distance(distance); + } + /** * Retrieves the results of the query. * * @return a ResultSet containing the query results */ @Override - public ResultSet results() { - return new ResultSet<>(World.getGroundItems().stream() + public GroundItemResultSet results() { + return cache.getOrCompute(() -> new GroundItemResultSet(World.getGroundItems().stream() .flatMap(itemStack -> itemStack.getItems().stream()) .filter(this) - .toList()); + .toList())); + } + + public GroundItem nearestWithin(double maxDistance) { + return results().nearestWithin(maxDistance); } /** @@ -75,6 +96,10 @@ public boolean test(GroundItem groundItem) { return this.root.test(groundItem); } + private void invalidateCache() { + cache.invalidate(); + } + // ========== Item-based filtering methods ========== /** @@ -87,6 +112,7 @@ public GroundItemQuery id(int... ids) { if (ids.length == 0) { return this; } + invalidateCache(); this.root = this.root.and(i -> Arrays.stream(ids).anyMatch(id -> id == i.getId())); return this; } @@ -99,6 +125,7 @@ public GroundItemQuery id(int... ids) { * @return the updated GroundItemQuery */ public GroundItemQuery quantity(BiFunction spred, int quantity) { + invalidateCache(); this.root = this.root.and(i -> spred.apply(i.getQuantity(), quantity)); return this; } @@ -123,6 +150,7 @@ public GroundItemQuery itemTypes(ItemDefinition... itemTypes) { if (itemTypes.length == 0) { return this; } + invalidateCache(); this.root = this.root.and(i -> Arrays.stream(itemTypes).anyMatch(itemType -> itemType.equals(i.getType()))); return this; } @@ -137,6 +165,7 @@ public GroundItemQuery category(int... categories) { if (categories.length == 0) { return this; } + invalidateCache(); this.root = this.root.and(i -> Arrays.stream(categories).anyMatch(category -> category == i.getCategory())); return this; } @@ -152,6 +181,7 @@ public GroundItemQuery name(BiFunction spred, Str if (names.length == 0) { return this; } + invalidateCache(); this.root = this.root.and(i -> Arrays.stream(names).anyMatch(name -> spred.apply(i.getName(), name))); return this; } @@ -176,6 +206,7 @@ public GroundItemQuery name(java.util.regex.Pattern... patterns) { if (patterns.length == 0) { return this; } + invalidateCache(); this.root = this.root.and(i -> { String itemName = i.getName(); return itemName != null && Arrays.stream(patterns).anyMatch(p -> p.matcher(itemName).matches()); @@ -193,6 +224,7 @@ public GroundItemQuery stackType(StackType... stackTypes) { if (stackTypes.length == 0) { return this; } + invalidateCache(); this.root = this.root.and(i -> Arrays.stream(stackTypes).anyMatch(stackType -> stackType == i.getStackType())); return this; } @@ -209,6 +241,7 @@ public GroundItemQuery coordinate(Coordinate... coordinates) { if (coordinates.length == 0) { return this; } + invalidateCache(); this.root = this.root.and(i -> Arrays.stream(coordinates).anyMatch(coord -> i.getStack().getCoordinate().equals(coord))); return this; } @@ -220,6 +253,7 @@ public GroundItemQuery coordinate(Coordinate... coordinates) { * @return the updated GroundItemQuery */ public GroundItemQuery inside(Area area) { + invalidateCache(); this.root = this.root.and(i -> area.contains(i.getStack().getCoordinate())); return this; } @@ -231,6 +265,7 @@ public GroundItemQuery inside(Area area) { * @return the updated GroundItemQuery */ public GroundItemQuery outside(Area area) { + invalidateCache(); this.root = this.root.and(i -> !area.contains(i.getStack().getCoordinate())); return this; } @@ -242,6 +277,7 @@ public GroundItemQuery outside(Area area) { * @return the updated GroundItemQuery */ public GroundItemQuery distance(double distance) { + invalidateCache(); this.root = this.root.and(i -> Distance.to(i.getStack().getCoordinate()) <= distance); return this; } @@ -253,6 +289,7 @@ public GroundItemQuery distance(double distance) { * @return the updated GroundItemQuery */ public GroundItemQuery valid(boolean valid) { + invalidateCache(); this.root = this.root.and(i -> i.getStack().isValid() == valid); return this; } @@ -266,6 +303,7 @@ public GroundItemQuery valid(boolean valid) { * @return the updated GroundItemQuery */ public GroundItemQuery and(GroundItemQuery other) { + invalidateCache(); this.root = this.root.and(other.root); return this; } @@ -277,6 +315,7 @@ public GroundItemQuery and(GroundItemQuery other) { * @return the updated GroundItemQuery */ public GroundItemQuery or(GroundItemQuery other) { + invalidateCache(); this.root = this.root.or(other.root); return this; } @@ -287,6 +326,7 @@ public GroundItemQuery or(GroundItemQuery other) { * @return the updated GroundItemQuery with negated predicate */ public GroundItemQuery invert() { + invalidateCache(); this.root = this.root.negate(); return this; } @@ -301,3 +341,5 @@ public GroundItemQuery mark() { return this; } } + + diff --git a/src/main/java/net/botwithus/xapi/query/InventoryItemQuery.java b/src/main/java/net/botwithus/xapi/query/InventoryItemQuery.java index f54ba6a..f566325 100644 --- a/src/main/java/net/botwithus/xapi/query/InventoryItemQuery.java +++ b/src/main/java/net/botwithus/xapi/query/InventoryItemQuery.java @@ -2,74 +2,68 @@ import net.botwithus.rs3.item.InventoryItem; import net.botwithus.xapi.query.base.ItemQuery; +import net.botwithus.xapi.query.base.QueryCache; import net.botwithus.xapi.query.result.ResultSet; -import java.util.Arrays; +import java.time.Duration; +import java.util.Collections; import java.util.Iterator; -/** - * A query class for filtering and retrieving inventory items based on various criteria. - */ public class InventoryItemQuery extends ItemQuery { - /** - * Constructs a new InvItemQuery with the specified inventory IDs. - * - * @param inventoryId the inventory IDs to query - */ + private final QueryCache> cache = new QueryCache<>(); + public InventoryItemQuery(int... inventoryId) { super(inventoryId); } - /** - * 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); } - /** - * Retrieves the results of the query as a ResultSet. - * - * @return a ResultSet containing the filtered inventory items - */ + public static InventoryItemQuery stackableLoot(int inventoryId, int... itemIds) { + return newQuery(inventoryId).id(itemIds).stackType(net.botwithus.rs3.cache.assets.items.StackType.ALWAYS); + } + + public InventoryItemQuery withCache(Duration ttl) { + cache.configure(ttl); + inventoryQuery.withCache(ttl); + return this; + } + @Override public ResultSet results() { - return new ResultSet<>(inventoryQuery.results().first().getItems().stream().filter(this).toList()); + return cache.getOrCompute(() -> { + var inventory = inventoryQuery.results().first(); + if (inventory == null) { + return new ResultSet<>(Collections.emptyList()); + } + return new ResultSet<>(inventory.getItems().stream().filter(this).toList()); + }); } - /** - * 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 - */ + @Override + protected void predicateChanged() { + cache.invalidate(); + super.predicateChanged(); + } + public InventoryItemQuery slot(int... slots) { - this.root = this.root.and(t -> Arrays.stream(slots).anyMatch(i -> i == t.getSlot())); + if (slots.length == 0) { + return this; + } + this.root = this.root.and(t -> java.util.Arrays.stream(slots).anyMatch(i -> i == t.getSlot())); + predicateChanged(); return this; } -} \ 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..1b333fc 100644 --- a/src/main/java/net/botwithus/xapi/query/InventoryQuery.java +++ b/src/main/java/net/botwithus/xapi/query/InventoryQuery.java @@ -5,27 +5,22 @@ import net.botwithus.rs3.inventories.InventoryManager; import net.botwithus.rs3.item.Item; import net.botwithus.xapi.query.base.Query; +import net.botwithus.xapi.query.base.QueryCache; import net.botwithus.xapi.query.result.ResultSet; +import java.time.Duration; import java.util.Arrays; import java.util.Iterator; import java.util.Objects; 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> { protected Predicate root; + private final QueryCache> cache = new QueryCache<>(); private int[] ids; - /** - * Constructs a new InventoryQuery with the specified IDs. - * - * @param ids the IDs to query - */ public InventoryQuery(int... ids) { this.ids = ids; if (ids.length == 0) { @@ -35,109 +30,69 @@ public InventoryQuery(int... ids) { } } - /** - * Retrieves the results of the query as a ResultSet. - * - * @return a ResultSet containing the filtered inventories - */ + public InventoryQuery withCache(Duration ttl) { + cache.configure(ttl); + return this; + } + + public static InventoryQuery containingAny(int inventoryId, int... itemIds) { + return new InventoryQuery(inventoryId).contains(itemIds); + } + @Override public ResultSet results() { - return new ResultSet<>( + return cache.getOrCompute(() -> 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 - */ + .mapToObj(InventoryManager::getInventory) + .filter(Objects::nonNull) + .filter(this) + .toList() + )); + } + @Override public Iterator iterator() { return results().iterator(); } - /** - * 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) { + invalidateCache(); root = root.and(t -> Arrays.stream(types).anyMatch(i -> i == t.getDefinition())); return this; } - /** - * Filters inventories by full status. - * - * @param full the full status to filter by - * @return the updated InventoryQuery - */ public InventoryQuery isFull(boolean full) { + invalidateCache(); root = root.and(t -> t.isFull() == 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) { + invalidateCache(); root = root.and(t -> func.apply(t.freeSlots(), 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); } - /** - * Filters inventories by item IDs. - * - * @param itemIds the item IDs to filter by - * @return the updated InventoryQuery - */ public InventoryQuery contains(int... itemIds) { + invalidateCache(); root = root.and(t -> Arrays.stream(itemIds).anyMatch(t::contains)); 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; } + invalidateCache(); 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))); @@ -145,46 +100,29 @@ public InventoryQuery contains(BiFunction spred, 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); } - /** - * 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) { + invalidateCache(); root = root.and(t -> Arrays.stream(itemIds).allMatch(t::contains)); return this; } - /** - * Filters inventories by item categories. - * - * @param categories the item categories to filter by - * @return the updated InventoryQuery - */ public InventoryQuery containsCategory(int... categories) { + invalidateCache(); root = root.and(t -> Arrays.stream(categories).anyMatch(t::containsByCategory)); 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) { + invalidateCache(); root = root.and(t -> Arrays.stream(categories).allMatch(t::containsByCategory)); return this; } -} \ No newline at end of file + + private void invalidateCache() { + cache.invalidate(); + } +} diff --git a/src/main/java/net/botwithus/xapi/query/NpcQuery.java b/src/main/java/net/botwithus/xapi/query/NpcQuery.java index 00a2291..e683057 100644 --- a/src/main/java/net/botwithus/xapi/query/NpcQuery.java +++ b/src/main/java/net/botwithus/xapi/query/NpcQuery.java @@ -3,51 +3,47 @@ import net.botwithus.rs3.entities.PathingEntity; import net.botwithus.rs3.world.World; import net.botwithus.xapi.query.base.PathingEntityQuery; +import net.botwithus.xapi.query.base.QueryCache; import net.botwithus.xapi.query.result.EntityResultSet; -import net.botwithus.xapi.query.result.ResultSet; +import java.time.Duration; import java.util.Iterator; public class NpcQuery extends PathingEntityQuery { - /** - * Creates a new NpcQuery instance. - * - * @return a new NpcQuery instance - */ + private final QueryCache> cache = new QueryCache<>(); + public static NpcQuery newQuery() { return new NpcQuery(); } - /** - * Retrieves the results of the query. - * - * @return a ResultSet containing the query results - */ + public static NpcQuery hostileWithin(double distance, String... names) { + return newQuery().name(names).distance(distance).valid(true); + } + + public NpcQuery withCache(Duration ttl) { + cache.configure(ttl); + return this; + } + @Override public EntityResultSet results() { - return new EntityResultSet(World.getNpcs().stream().filter(this).toList()); + return cache.getOrCompute(() -> new EntityResultSet<>(World.getNpcs().stream().filter(this).toList())); } - /** - * Returns an iterator over the query results. - * - * @return an Iterator over the query results - */ @Override 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); } -} \ No newline at end of file + @Override + protected void predicateChanged() { + cache.invalidate(); + super.predicateChanged(); + } +} diff --git a/src/main/java/net/botwithus/xapi/query/SceneObjectQuery.java b/src/main/java/net/botwithus/xapi/query/SceneObjectQuery.java index fd272c1..7ddc263 100644 --- a/src/main/java/net/botwithus/xapi/query/SceneObjectQuery.java +++ b/src/main/java/net/botwithus/xapi/query/SceneObjectQuery.java @@ -4,132 +4,94 @@ import net.botwithus.rs3.entities.SceneObject; import net.botwithus.rs3.world.World; import net.botwithus.xapi.query.base.EntityQuery; +import net.botwithus.xapi.query.base.QueryCache; import net.botwithus.xapi.query.result.EntityResultSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.Duration; import java.util.Arrays; import java.util.Iterator; +import java.util.Objects; import java.util.function.BiFunction; public class SceneObjectQuery extends EntityQuery { private static final Logger logger = LoggerFactory.getLogger(SceneObjectQuery.class); + private final QueryCache> cache = new QueryCache<>(); - /** - * Creates a new SceneObjectQuery instance. - * - * @return a new SceneObjectQuery instance - */ public static SceneObjectQuery newQuery() { return new SceneObjectQuery(); } - /** - * Retrieves the results of the query. - * - * @return a ResultSet containing the query results - */ + public static SceneObjectQuery interactable(int... typeIds) { + return newQuery().typeId(typeIds).hidden(false); + } + + public SceneObjectQuery withCache(Duration ttl) { + cache.configure(ttl); + return this; + } + @Override public EntityResultSet results() { - return new EntityResultSet<>(World.getSceneObjects().stream().filter(this).toList()); + return cache.getOrCompute(() -> new EntityResultSet<>(World.getSceneObjects().stream().filter(this).toList())); } - /** - * Returns an iterator over the query results. - * - * @return an Iterator over the query results - */ @Override public Iterator iterator() { return results().iterator(); } - /** - * 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); } - /** - * 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)); + predicateChanged(); 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)); + predicateChanged(); 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); + predicateChanged(); 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)); + predicateChanged(); 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()))); + this.root = this.root.and(t -> Arrays.stream(names) + .filter(Objects::nonNull) + .anyMatch(i -> spred.apply(i, t.getName()))); + predicateChanged(); 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); } @@ -142,43 +104,28 @@ public SceneObjectQuery name(java.util.regex.Pattern... patterns) { String objName = t.getName(); return objName != null && Arrays.stream(patterns).anyMatch(p -> p.matcher(objName).matches()); }); + predicateChanged(); 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; } 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 objOptions != null && Arrays.stream(options) + .filter(Objects::nonNull) + .anyMatch(i -> objOptions.stream().anyMatch(j -> j != null && spred.apply(i, j))); }); + predicateChanged(); return this; } - /** - * 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; @@ -186,10 +133,15 @@ public SceneObjectQuery option(java.util.regex.Pattern... patterns) { 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()) - ); + Arrays.stream(patterns).anyMatch(p -> p.matcher(opt).matches())); }); + predicateChanged(); return this; } -} \ No newline at end of file + @Override + protected void predicateChanged() { + cache.invalidate(); + super.predicateChanged(); + } +} diff --git a/src/main/java/net/botwithus/xapi/query/base/EntityQuery.java b/src/main/java/net/botwithus/xapi/query/base/EntityQuery.java index 55128e0..b8bc8ed 100644 --- a/src/main/java/net/botwithus/xapi/query/base/EntityQuery.java +++ b/src/main/java/net/botwithus/xapi/query/base/EntityQuery.java @@ -24,126 +24,93 @@ public EntityQuery() { } /** - * Filters entities by type. - * - * @param entityType the entity types to filter by - * @return the updated EntityQuery + * Hook invoked whenever the predicate chain changes. Subclasses can override to invalidate caches. */ + protected void predicateChanged() { + // default no-op + } + @SuppressWarnings("unchecked") public > Q type(EntityType... entityType) { + if (entityType.length == 0) { + return (Q) this; + } this.root = this.root.and(t -> Arrays.stream(entityType).anyMatch(i -> t.getType() == i)); + predicateChanged(); 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) { + if (coordinate.length == 0) { + return (Q) this; + } this.root = this.root.and(t -> Arrays.stream(coordinate).anyMatch(i -> t.getCoordinate().equals(i))); + predicateChanged(); 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) { + if (vector3f.length == 0) { + return (Q) this; + } this.root = this.root.and(t -> Arrays.stream(vector3f).anyMatch(i -> t.getDirection().equals(i))); + predicateChanged(); 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); + predicateChanged(); 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())); + predicateChanged(); 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())); + predicateChanged(); return (Q) this; } @SuppressWarnings("unchecked") public > Q distance(double distance) { this.root = this.root.and(t -> Distance.to(t) <= distance); + predicateChanged(); 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); + predicateChanged(); 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); + predicateChanged(); 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(); + predicateChanged(); 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 index 5faf6ae..f08ae5e 100644 --- a/src/main/java/net/botwithus/xapi/query/base/ItemQuery.java +++ b/src/main/java/net/botwithus/xapi/query/base/ItemQuery.java @@ -13,45 +13,65 @@ public abstract class ItemQuery> implements Query> { protected Predicate root; - - protected InventoryQuery inventoryQuery; + protected final InventoryQuery inventoryQuery; public ItemQuery(int... inventoryId) { inventoryQuery = new InventoryQuery(inventoryId); - root = item -> true; // Initialize with a predicate that accepts all items + root = item -> true; + } + + protected void predicateChanged() { + // subclasses override to invalidate caches } @SuppressWarnings("unchecked") public T id(int... ids) { + if (ids.length == 0) { + return (T) this; + } root = root.and(i -> Arrays.stream(ids).anyMatch(id -> id == i.getId())); + predicateChanged(); return (T) this; } @SuppressWarnings("unchecked") public T quantity(BiFunction spred, int quantity) { root = root.and(i -> spred.apply(i.getQuantity(), quantity)); + predicateChanged(); return (T) this; } public T quantity(int quantity) { - return quantity((a, b) -> a == b, quantity); + return quantity((a, b) -> a.equals(b), quantity); } @SuppressWarnings("unchecked") public T itemTypes(ItemDefinition... itemTypes) { + if (itemTypes.length == 0) { + return (T) this; + } root = root.and(i -> Arrays.stream(itemTypes).anyMatch(itemType -> itemType == i.getType())); + predicateChanged(); return (T) this; } @SuppressWarnings("unchecked") public T category(int... categories) { + if (categories.length == 0) { + return (T) this; + } root = root.and(i -> Arrays.stream(categories).anyMatch(category -> category == i.getCategory())); + predicateChanged(); return (T) this; } @SuppressWarnings("unchecked") public T name(BiFunction spred, String... names) { + if (names.length == 0) { + return (T) this; + } root = root.and(i -> Arrays.stream(names).anyMatch(name -> spred.apply(i.getName(), name))); + predicateChanged(); return (T) this; } @@ -61,16 +81,24 @@ public T name(String... names) { @SuppressWarnings("unchecked") public T name(java.util.regex.Pattern... patterns) { + if (patterns.length == 0) { + return (T) this; + } root = root.and(i -> { String itemName = i.getName(); return itemName != null && Arrays.stream(patterns).anyMatch(p -> p.matcher(itemName).matches()); }); + predicateChanged(); return (T) this; } @SuppressWarnings("unchecked") public T stackType(StackType... stackTypes) { + if (stackTypes.length == 0) { + return (T) this; + } root = root.and(i -> Arrays.stream(stackTypes).anyMatch(stackType -> stackType == i.getStackType())); + predicateChanged(); 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 index 8d67bd4..ae83b68 100644 --- a/src/main/java/net/botwithus/xapi/query/base/PathingEntityQuery.java +++ b/src/main/java/net/botwithus/xapi/query/base/PathingEntityQuery.java @@ -2,52 +2,37 @@ 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.Objects; 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)); + predicateChanged(); 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)); + predicateChanged(); 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))); + this.root = this.root.and(t -> Arrays.stream(names).anyMatch(n -> n != null && n.equals(t.getName()))); + predicateChanged(); return this; } @@ -59,150 +44,102 @@ public PathingEntityQuery name(java.util.regex.Pattern... patterns) { String entityName = t.getName(); return entityName != null && Arrays.stream(patterns).anyMatch(p -> p.matcher(entityName).matches()); }); + predicateChanged(); 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))); + this.root = this.root.and(t -> Arrays.stream(overheadTexts) + .anyMatch(text -> text != null && text.equals(t.getOverheadText()))); + predicateChanged(); 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); + predicateChanged(); 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)); + predicateChanged(); 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)); + predicateChanged(); 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); + predicateChanged(); 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); + predicateChanged(); 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())); + if (entity.length == 0) { + return this; + } + this.root = this.root.and(t -> Arrays.stream(entity) + .filter(Objects::nonNull) + .anyMatch(e -> t.getFollowingType() == e.getType() && t.getFollowingIndex() == e.getIndex())); + predicateChanged(); 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)); + predicateChanged(); 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)); + predicateChanged(); 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) { + if (option.length == 0) { + return this; + } 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 options != null && Arrays.stream(option) + .filter(Objects::nonNull) + .anyMatch(i -> options.stream().anyMatch(j -> j != null && spred.apply(i, j))); }); + predicateChanged(); 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) { @@ -211,9 +148,9 @@ public PathingEntityQuery option(java.util.regex.Pattern... patterns) { 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()) - ); + Arrays.stream(patterns).anyMatch(p -> p.matcher(opt).matches())); }); + predicateChanged(); return this; } -} \ No newline at end of file +} diff --git a/src/main/java/net/botwithus/xapi/query/base/Query.java b/src/main/java/net/botwithus/xapi/query/base/Query.java index 404423b..84bbee4 100644 --- a/src/main/java/net/botwithus/xapi/query/base/Query.java +++ b/src/main/java/net/botwithus/xapi/query/base/Query.java @@ -1,9 +1,34 @@ package net.botwithus.xapi.query.base; import net.botwithus.xapi.query.result.ResultSet; +import net.botwithus.xapi.script.permissive.Permissive; +import java.util.Objects; import java.util.function.Predicate; -public interface Query > extends Iterable, Predicate { +public interface Query> extends Iterable, Predicate { R results(); + + /** + * Bridges this query into a permissive predicate that succeeds when the query yields results. + * + * @param name human-readable identifier for the permissive + * @return permissive evaluating this query each tick + */ + default Permissive asPermissive(String name) { + return asPermissive(name, resultSet -> !resultSet.isEmpty()); + } + + /** + * Bridges this query into a permissive using the provided condition. + * + * @param name human-readable identifier for the permissive + * @param satisfied predicate evaluated against the latest query results + * @return permissive evaluating this query each tick + */ + default Permissive asPermissive(String name, Predicate satisfied) { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(satisfied, "satisfied"); + return new Permissive(name, () -> satisfied.test(results())); + } } diff --git a/src/main/java/net/botwithus/xapi/query/base/QueryCache.java b/src/main/java/net/botwithus/xapi/query/base/QueryCache.java new file mode 100644 index 0000000..0549cfc --- /dev/null +++ b/src/main/java/net/botwithus/xapi/query/base/QueryCache.java @@ -0,0 +1,84 @@ +package net.botwithus.xapi.query.base; + +import java.time.Duration; +import java.time.Instant; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Simple TTL-based cache support for query results. + * @param cached result type + */ +public final class QueryCache { + + private Duration ttl; + private Instant expiresAt = Instant.EPOCH; + private R cachedValue; + + /** + * Configures the cache TTL. Passing {@code null} or a non-positive duration disables caching. + * + * @param ttl time-to-live for cached results + */ + public synchronized void configure(Duration ttl) { + this.ttl = normalize(ttl); + invalidate(); + } + + /** + * @return true when caching is enabled + */ + public synchronized boolean isEnabled() { + return ttl != null; + } + + /** + * Clears any cached value immediately. + */ + public synchronized void invalidate() { + cachedValue = null; + expiresAt = Instant.EPOCH; + } + + /** + * Returns the cached value if present and still valid, otherwise computes a fresh value. + * + * @param supplier computation used when the cache is empty or expired + * @return cached or freshly-computed value + */ + public R getOrCompute(Supplier supplier) { + Objects.requireNonNull(supplier, "supplier"); + Duration localTtl; + Instant localExpiry; + R localValue; + synchronized (this) { + localTtl = this.ttl; + localExpiry = this.expiresAt; + localValue = this.cachedValue; + } + + if (localTtl == null) { + return supplier.get(); + } + + var now = Instant.now(); + if (localValue == null || now.isAfter(localExpiry)) { + synchronized (this) { + now = Instant.now(); + if (cachedValue == null || now.isAfter(expiresAt)) { + cachedValue = supplier.get(); + expiresAt = now.plus(localTtl); + } + return cachedValue; + } + } + return localValue; + } + + private static Duration normalize(Duration ttl) { + if (ttl == null || ttl.isZero() || ttl.isNegative()) { + return null; + } + return ttl; + } +} 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..6a71c04 100644 --- a/src/main/java/net/botwithus/xapi/query/result/EntityResultSet.java +++ b/src/main/java/net/botwithus/xapi/query/result/EntityResultSet.java @@ -6,9 +6,11 @@ import net.botwithus.rs3.world.Distance; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.Optional; public class EntityResultSet extends ResultSet { @@ -21,6 +23,35 @@ public EntityResultSet(List results) { super(results); } + /** + * Wraps a collection into an EntityResultSet, filtering out nulls. + * + * @param entities source entities + * @param entity type + * @return result set with non-null entities + */ + public static EntityResultSet wrap(Collection entities) { + Objects.requireNonNull(entities, "entities"); + return new EntityResultSet<>(entities.stream().filter(Objects::nonNull).toList()); + } + + /** + * Wraps a collection into an EntityResultSet sorted by distance to the supplied coordinate. + * + * @param entities source entities + * @param coordinate reference coordinate + * @param entity type + * @return result set sorted by increasing distance + */ + public static EntityResultSet sortedByDistance(Collection entities, Coordinate coordinate) { + Objects.requireNonNull(entities, "entities"); + Objects.requireNonNull(coordinate, "coordinate"); + return new EntityResultSet<>(entities.stream() + .filter(Objects::nonNull) + .sorted(distanceComparator(coordinate)) + .toList()); + } + /** * Finds the nearest entity to the given coordinate. * @@ -28,13 +59,70 @@ public EntityResultSet(List results) { * @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); + if (coordinate == null) { + return null; + } + return sortedBy(coordinate).map(list -> list.get(0)).orElse(null); + } + + /** + * Finds the nearest entity to the given coordinate within the provided distance. + * + * @param coordinate reference coordinate + * @param maxDistance maximum allowed distance + * @return nearest entity within distance or {@code null} + */ + public T nearestWithin(Coordinate coordinate, double maxDistance) { + if (coordinate == null) { + return null; + } + return filteredByDistance(coordinate, maxDistance).map(list -> list.get(0)).orElse(null); + } + + /** + * 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; + } + + /** + * Finds the nearest entity to the given entity within the provided distance. + * + * @param entity reference entity + * * @param maxDistance maximum allowed distance + * @return nearest entity within distance or {@code null} + */ + public T nearestWithin(Entity entity, double maxDistance) { + return entity != null ? nearestWithin(entity.getCoordinate(), maxDistance) : null; + } + + /** + * 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); + } + return null; + } + + /** + * Finds the nearest entity to the local player within the provided distance. + * + * @param maxDistance maximum allowed distance + * @return nearest entity within distance or {@code null} + */ + public T nearestWithin(double maxDistance) { + var player = LocalPlayer.self(); + if (player != null) { + return nearestWithin(player, maxDistance); } return null; } @@ -63,26 +151,28 @@ public EntityResultSet remove(T 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 Optional> sortedBy(Coordinate coordinate) { + if (results.isEmpty()) { + return Optional.empty(); + } + List copy = new ArrayList<>(results); + copy.removeIf(Objects::isNull); + copy.sort(distanceComparator(coordinate)); + return copy.isEmpty() ? Optional.empty() : Optional.of(copy); } - /** - * 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); + private Optional> filteredByDistance(Coordinate coordinate, double maxDistance) { + if (results.isEmpty()) { + return Optional.empty(); } - return null; + List copy = new ArrayList<>(results); + copy.removeIf(Objects::isNull); + copy.removeIf(entity -> Distance.between(entity, coordinate) > maxDistance); + copy.sort(distanceComparator(coordinate)); + return copy.isEmpty() ? Optional.empty() : Optional.of(copy); + } + + private static Comparator distanceComparator(Coordinate coordinate) { + return Comparator.comparingDouble(entity -> Distance.between(entity, coordinate)); } -} \ No newline at end of file +} diff --git a/src/main/java/net/botwithus/xapi/query/result/GroundItemResultSet.java b/src/main/java/net/botwithus/xapi/query/result/GroundItemResultSet.java new file mode 100644 index 0000000..3654589 --- /dev/null +++ b/src/main/java/net/botwithus/xapi/query/result/GroundItemResultSet.java @@ -0,0 +1,37 @@ +package net.botwithus.xapi.query.result; + +import net.botwithus.rs3.entities.LocalPlayer; +import net.botwithus.rs3.item.GroundItem; +import net.botwithus.rs3.world.Coordinate; +import net.botwithus.rs3.world.Distance; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +public class GroundItemResultSet extends ResultSet { + + public GroundItemResultSet(List results) { + super(results); + } + + public GroundItem nearestWithin(double maxDistance) { + var player = LocalPlayer.self(); + if (player == null) { + return null; + } + return nearestWithin(player.getCoordinate(), maxDistance); + } + + public GroundItem nearestWithin(Coordinate coordinate, double maxDistance) { + if (coordinate == null) { + return null; + } + return results.stream() + .filter(Objects::nonNull) + .filter(item -> item.getStack() != null && item.getStack().getCoordinate() != null) + .filter(item -> Distance.between(item.getStack().getCoordinate(), coordinate) <= maxDistance) + .min(Comparator.comparingDouble(item -> Distance.between(item.getStack().getCoordinate(), coordinate))) + .orElse(null); + } +} 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..8edc881 100644 --- a/src/main/java/net/botwithus/xapi/query/result/ResultSet.java +++ b/src/main/java/net/botwithus/xapi/query/result/ResultSet.java @@ -4,6 +4,10 @@ import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; public class ResultSet implements Iterable { @@ -77,6 +81,18 @@ public Stream stream() { return results.stream(); } + /** + * Groups elements using the provided classifier. + * + * @param classifier grouping function + * @param key type + * @return grouped elements + */ + public Map> groupBy(Function classifier) { + Objects.requireNonNull(classifier, "classifier"); + return results.stream().collect(Collectors.groupingBy(classifier)); + } + /** * Returns the number of elements in the result set. * @@ -94,4 +110,4 @@ public int size() { public boolean isEmpty() { return results.isEmpty(); } -} \ No newline at end of file +} 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..9b6b8b6 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 @@ -19,7 +19,7 @@ public abstract class PermissiveScript extends DelayableScript { - private boolean debugMode = false; + boolean debugMode = false; private State currentState; // Map for states, with a name key for each state @@ -33,6 +33,47 @@ public abstract class PermissiveScript extends DelayableScript { // Logger instance for this script protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + private static final String LOGBACK_LOGGER_CLASS = "ch.qos.logback.classic.Logger"; + private static final String LOGBACK_LEVEL_CLASS = "ch.qos.logback.classic.Level"; + private static final String LOGBACK_CONTEXT_CLASS = "ch.qos.logback.classic.LoggerContext"; + + private static void applyDebugLoggingConfiguration(boolean debugEnabled, Logger referenceLogger) { + if (!configureLogback(debugEnabled, referenceLogger)) { + if (debugEnabled) { + referenceLogger.warn("Debug mode enabled but no supported logging backend was detected for level updates."); + } + } + } + + private static boolean configureLogback(boolean debugEnabled, Logger referenceLogger) { + try { + Class contextClass = Class.forName(LOGBACK_CONTEXT_CLASS); + Object context = LoggerFactory.getILoggerFactory(); + if (!contextClass.isInstance(context)) { + return false; + } + + Class levelClass = Class.forName(LOGBACK_LEVEL_CLASS); + Object level = levelClass.getField(debugEnabled ? "DEBUG" : "INFO").get(null); + Class loggerClass = Class.forName(LOGBACK_LOGGER_CLASS); + + Object rootLogger = contextClass.getMethod("getLogger", String.class) + .invoke(context, org.slf4j.Logger.ROOT_LOGGER_NAME); + if (!loggerClass.isInstance(rootLogger)) { + return false; + } + + loggerClass.getMethod("setLevel", levelClass).invoke(rootLogger, level); + referenceLogger.debug("Updated Logback root logger level to {}", debugEnabled ? "DEBUG" : "INFO"); + return true; + } catch (ClassNotFoundException ignored) { + return false; + } catch (ReflectiveOperationException ex) { + referenceLogger.warn("Failed to update Logback logger level: {}", ex.getMessage(), ex); + return false; + } + } + /*** * Main game tick logic */ @@ -198,6 +239,7 @@ public boolean isDebugMode() { public void setDebugMode(boolean debugMode) { this.debugMode = debugMode; + applyDebugLoggingConfiguration(debugMode, logger); }