, + E extends IEntity + > + implements IEntityQueryable{ + + protected Streamsource; + + protected AbstractEntityQueryable() { + this.source = initialSource(); + } + + protected abstract Stream initialSource(); + + @SuppressWarnings("unchecked") + @Override + public Q fromWorldView() { + var worldView = new Rs2PlayerModel().getWorldView(); + if (worldView == null) { + this.source = Stream.empty(); + return (Q) this; + } + + this.source = this.source + .filter(o -> o.getWorldView() != null && o.getWorldView().getId() == worldView.getId()); + + return (Q) this; + } + + @SuppressWarnings("unchecked") + @Override + public Q where(Predicate predicate) { + source = source.filter(predicate); + return (Q) this; + } + + @SuppressWarnings("unchecked") + @Override + public Q within(int distance) { + WorldPoint playerLoc = new Rs2PlayerModel().getWorldLocation(); + if (playerLoc == null) { + this.source = Stream.empty(); + return (Q) this; + } + + this.source = this.source + .filter(o -> o.getWorldLocation().distanceTo(playerLoc) <= distance); + + return (Q) this; + } + + @SuppressWarnings("unchecked") + @Override + public Q within(WorldPoint anchor, int distance) { + if (anchor == null) { + this.source = Stream.empty(); + return (Q) this; + } + + this.source = this.source + .filter(o -> o.getWorldLocation().distanceTo(anchor) <= distance); + + return (Q) this; + } + + @SuppressWarnings("unchecked") + @Override + public Q withName(String name) { + if (name == null) { + this.source = Stream.empty(); + return (Q) this; + } + + this.source = this.source.filter(x -> { + String n = x.getName(); + return n != null && n.equalsIgnoreCase(name); + }); + + return (Q) this; + } + + @SuppressWarnings("unchecked") + @Override + public Q withNames(String... names) { + if (names == null || names.length == 0) { + this.source = Stream.empty(); + return (Q) this; + } + + this.source = this.source.filter(x -> { + String n = x.getName(); + if (n == null) return false; + return Arrays.stream(names).anyMatch(n::equalsIgnoreCase); + }); + + return (Q) this; + } + + @SuppressWarnings("unchecked") + @Override + public Q withId(int id) { + this.source = this.source.filter(x -> x.getId() == id); + return (Q) this; + } + + @SuppressWarnings("unchecked") + @Override + public Q withIds(int... ids) { + if (ids == null || ids.length == 0) { + this.source = Stream.empty(); + return (Q) this; + } + + this.source = this.source.filter(x -> { + int entityId = x.getId(); + for (int id : ids) { + if (entityId == id) return true; + } + return false; + }); + + return (Q) this; + } + + @Override + public E first() { + return source.findFirst().orElse(null); + } + + @Override + public E firstReachable() { + return source.filter(IEntity::isReachable).findFirst().orElse(null); + } + + @Override + public E nearest() { + return nearest(Integer.MAX_VALUE); + } + + @Override + public E nearestReachable() { + source = source.filter(IEntity::isReachable); + return nearest(Integer.MAX_VALUE); + } + + @Override + public E nearestReachable(int maxDistance) { + source = source.filter(IEntity::isReachable); + return nearest(maxDistance); + } + + @Override + public E nearest(int maxDistance) { + var player = new Rs2PlayerModel(); + WorldPoint playerLoc = player.getWorldLocation(); + WorldView worldView = player.getWorldView(); + if (playerLoc == null || worldView == null) { + return null; + } + + return nearest(playerLoc, maxDistance); + } + + @Override + public E nearest(WorldPoint anchor, int maxDistance) { + if (anchor == null) { + return null; + } + + return source + .map(entity -> { + WorldPoint loc = entity.getWorldLocation(); + int distance = (loc != null) ? loc.distanceTo(anchor) : Integer.MAX_VALUE; + return new EntityDistance<>(entity, distance); + }) + .filter(pair -> pair.distance <= maxDistance) + .min(Comparator.comparingInt(pair -> pair.distance)) + .map(pair -> pair.entity) + .orElse(null); + } + + @Override + public List toList() { + return source.collect(Collectors.toList()); + } + + public E firstOnClientThread() { + return Microbot.getClientThread().invoke(() -> first()); + } + + public E nearestOnClientThread() { + return Microbot.getClientThread().invoke(() -> nearest()); + } + + public E nearestOnClientThread(int maxDistance) { + return Microbot.getClientThread().invoke(() -> nearest(maxDistance)); + } + + public E nearestOnClientThread(WorldPoint anchor, int maxDistance) { + return Microbot.getClientThread().invoke(() -> nearest(anchor, maxDistance)); + } + + public List toListOnClientThread() { + return Microbot.getClientThread().invoke(() -> toList()); + } + + public boolean interact() { + E entity = nearestReachable(); + if (entity == null) return false; + + return entity.click(); + } + + public boolean interact(String action) { + E entity = nearestReachable(); + if (entity == null) return false; + return entity.click(action); + } + + public boolean interact(String action, int maxDistance) { + E entity = nearestReachable(maxDistance); + if (entity == null) return false; + return entity.click(action); + } + + public boolean interact(int id) { + E entity = this.withId(id).nearestReachable(); + if (entity == null) return false; + return entity.click(); + } + + public boolean interact(int id, String action) { + E entity = this.withId(id).nearestReachable(); + if (entity == null) return false; + return entity.click(action); + } + + public boolean interact(int id, String action, int maxDistance) { + E entity = this.withId(id).nearestReachable(maxDistance); + if (entity == null) return false; + return entity.click(action); + } +} + + + +class EntityDistance { + final E entity; + final int distance; + + EntityDistance(E entity, int distance) { + this.entity = entity; + this.distance = distance; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntity.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntity.java new file mode 100644 index 00000000000..c7d3e62d1bc --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntity.java @@ -0,0 +1,19 @@ +package net.runelite.client.plugins.microbot.api; + +import net.runelite.api.WorldView; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.util.reachable.Rs2Reachable; + +public interface IEntity { + int getId(); + String getName(); + WorldPoint getWorldLocation(); + LocalPoint getLocalLocation(); + WorldView getWorldView(); + boolean click(); + boolean click(String action); + default boolean isReachable() { + return Rs2Reachable.isReachable(getWorldLocation()); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntityQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntityQueryable.java new file mode 100644 index 00000000000..fe116ff1a68 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntityQueryable.java @@ -0,0 +1,59 @@ +package net.runelite.client.plugins.microbot.api; + +import net.runelite.api.WorldView; +import net.runelite.api.coords.WorldPoint; + +import java.util.List; +import java.util.function.Predicate; + +public interface IEntityQueryable , E extends IEntity> { + Q fromWorldView(); + Q where(Predicatepredicate); + + Q within(int distance); + + Q within(WorldPoint anchor, int distance); + + Q withName(String name); + + Q withNames(String... names); + + Q withId(int id); + + Q withIds(int... ids); + + E first(); + + E firstReachable(); + + E nearest(); + E nearestReachable(); + E nearestReachable(int maxDistance); + E nearest(int maxDistance); + + E nearest(WorldPoint anchor, int maxDistance); + + List toList(); + + E firstOnClientThread(); + + E nearestOnClientThread(); + + E nearestOnClientThread(int maxDistance); + + E nearestOnClientThread(WorldPoint anchor, int maxDistance); + + List toListOnClientThread(); + + boolean interact(); + + boolean interact(String action); + + boolean interact(String action, int maxDistance); + + boolean interact(int id); + + boolean interact(int id, String action); + + boolean interact(int id, String action, int maxDistance); +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/QUERYABLE_API.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/QUERYABLE_API.md new file mode 100644 index 00000000000..534a972d86b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/QUERYABLE_API.md @@ -0,0 +1,1153 @@ +# Queryable API Documentation + +**Version:** 2.1.0 +**Last Updated:** November 18, 2025 + +--- + +## Table of Contents + +1. [Introduction](#introduction) +2. [Why Use Queryable API?](#why-use-queryable-api) +3. [Core Concepts](#core-concepts) +4. [Getting Started](#getting-started) +5. [Available Queryables](#available-queryables) +6. [API Reference](#api-reference) +7. [Common Patterns](#common-patterns) +8. [Advanced Usage](#advanced-usage) +9. [Performance Tips](#performance-tips) +10. [Migration Guide](#migration-guide) + +--- + +## Introduction + +The **Queryable API** is a modern, fluent interface for querying game entities in Microbot. It provides a type-safe, chainable way to filter and find NPCs, players, ground items, and tile objects with minimal boilerplate code. + +### Design Philosophy + +- **Fluent Interface**: Chain multiple filters for readable code +- **Type-Safe**: Compile-time checking prevents errors +- **Performance**: Leverages efficient caching and streaming +- **Intuitive**: Natural language-like queries +- **Flexible**: Custom predicates for complex filters + +--- + +## Why Use Queryable API? + +### Old Way (Legacy) ❌ + +```java +// Verbose and hard to read +NPC banker = null; +for (NPC npc : client.getNpcs()) { + if (npc.getName() != null && + npc.getName().equals("Banker") && + !npc.getInteracting() != null) { + if (banker == null || + npc.getWorldLocation().distanceTo(player.getWorldLocation()) < + banker.getWorldLocation().distanceTo(player.getWorldLocation())) { + banker = npc; + } + } +} + +// Stream API - better but still verbose +NPC banker = Rs2Npc.getNpcs().stream() + .filter(npc -> npc.getName() != null) + .filter(npc -> npc.getName().equals("Banker")) + .filter(npc -> npc.getInteracting() == null) + .min(Comparator.comparingInt(npc -> + npc.getWorldLocation().distanceTo(player.getWorldLocation()))) + .orElse(null); +``` + +### New Way (Queryable) ✅ + +```java +// Clean, readable, and concise +Rs2NpcModel banker = new Rs2NpcQueryable() + .withName("Banker") + .where(npc -> !npc.isInteracting()) + .nearest(); +``` + +**Benefits:** +- 📖 **Readable**: Self-documenting code +- 🚀 **Faster Development**: Less code to write +- 🐛 **Fewer Bugs**: Type-safe operations +- 🔧 **Maintainable**: Easy to modify queries +- ⚡ **Performant**: Optimized internally + +--- + +## Core Concepts + +### 1. Entity Models + +All queryable entities implement the `IEntity` interface: + +```java +public interface IEntity { + String getName(); // Entity name + int getId(); // Entity ID + WorldPoint getWorldLocation(); // World coordinates + // ... other common properties +} +``` + +**Available Models:** +- `Rs2NpcModel` - NPCs +- `Rs2PlayerModel` - Players +- `Rs2TileItemModel` - Ground items +- `Rs2TileObjectModel` - Game objects + +### 2. Queryable Interface + +All queryables implement `IEntityQueryable `: + +```java +public interface IEntityQueryable{ + Q where(Predicatepredicate); // Custom filter + Q within(int distance); // Distance from player + Q within(WorldPoint anchor, int distance); // Distance from point + E first(); // First match + E nearest(); // Nearest to player + E nearest(int maxDistance); // Nearest within range + E nearest(WorldPoint anchor, int maxDistance); // Nearest to point + E withName(String name); // Find by name + E withNames(String... names); // Find by multiple names + E withId(int id); // Find by ID + E withIds(int... ids); // Find by multiple IDs + List toList(); // Get all matches +} +``` + +### 3. Fluent Chaining + +Methods return the queryable itself, allowing chaining: + +```java +new Rs2NpcQueryable() + .withName("Guard") // Filter by name + .where(npc -> !npc.isInteracting()) // Add custom filter + .within(15) // Within 15 tiles + .nearest(); // Get nearest match +``` + +### 4. Lazy Evaluation + +Queries are not executed until a terminal operation is called: + +```java +// No execution yet - just building the query +Rs2NpcQueryable query = new Rs2NpcQueryable() + .withName("Guard") + .within(10); + +// NOW it executes +Rs2NpcModel guard = query.nearest(); // Terminal operation +``` + +--- + +## Getting Started + +### Basic Query Structure + +Every query follows this pattern: + +```java +// 1. Create queryable +new Rs2NpcQueryable() + +// 2. Add filters (optional, chainable) + .withName("name") + .where(entity -> condition) + .within(distance) + +// 3. Execute with terminal operation + .nearest(); // or first(), toList(), etc. +``` + +### Simple Examples + +**Find nearest NPC by name:** +```java +Rs2NpcModel banker = new Rs2NpcQueryable() + .withName("Banker") + .nearest(); +``` + +**Find nearest ground item:** +```java +Rs2TileItemModel coins = new Rs2TileItemQueryable() + .withName("Coins") + .nearest(); +``` + +**Find nearest player:** +```java +Rs2PlayerModel player = new Rs2PlayerQueryable() + .withName("PlayerName") + .nearest(); +``` + +**Find nearest tree (tile object):** +```java +Rs2TileObjectModel tree = new Rs2TileObjectQueryable() + .withName("Tree") + .nearest(); +``` + +--- + +## Available Queryables + +### 1. Rs2NpcQueryable - NPC Queries + +**Import:** +```java +import net.runelite.client.plugins.microbot.api.npc.Rs2NpcQueryable; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; +``` + +**Basic Usage:** +```java +// Find nearest banker +Rs2NpcModel banker = new Rs2NpcQueryable() + .withName("Banker") + .nearest(); + +// Find all guards within 10 tiles +List guards = new Rs2NpcQueryable() + .withName("Guard") + .within(10) + .toList(); + +// Find nearest non-interacting cow +Rs2NpcModel cow = new Rs2NpcQueryable() + .withName("Cow") + .where(npc -> !npc.isInteracting()) + .nearest(); +``` + +**Rs2NpcModel Methods:** +```java +npc.getName() // "Guard" +npc.getId() // 123 +npc.getWorldLocation() // WorldPoint +npc.isInteracting() // true/false +npc.getHealthRatio() // 0-30 +npc.getAnimation() // Animation ID +npc.click("Attack") // Interact with NPC +npc.interact("Bank") // Alternative interact method +``` + +### 2. Rs2TileItemQueryable - Ground Item Queries + +**Import:** +```java +import net.runelite.client.plugins.microbot.api.tileitem.Rs2TileItemQueryable; +import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; +``` + +**Basic Usage:** +```java +// Find nearest coins +Rs2TileItemModel coins = new Rs2TileItemQueryable() + .withName("Coins") + .nearest(); + +// Find valuable items +List loot = new Rs2TileItemQueryable() + .where(item -> item.getTotalValue() > 1000) + .toList(); + +// Find nearest lootable item +Rs2TileItemModel lootable = new Rs2TileItemQueryable() + .where(Rs2TileItemModel::isLootAble) + .nearest(); +``` + +**Rs2TileItemModel Methods:** +```java +item.getName() // "Coins" +item.getId() // Item ID +item.getQuantity() // Stack size +item.getTotalValue() // GE value +item.getTotalGeValue() // Total GE value +item.isLootAble() // Can loot? +item.isOwned() // Owned by player? +item.isStackable() // Is stackable? +item.isNoted() // Is noted? +item.isTradeable() // Is tradeable? +item.isMembers() // Members item? +item.isDespawned() // Has despawned? +item.willDespawnWithin(seconds) // Will despawn soon? +item.pickup() // Pick up item +``` + +### 3. Rs2PlayerQueryable - Player Queries + +**Import:** +```java +import net.runelite.client.plugins.microbot.api.player.Rs2PlayerQueryable; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; +``` + +**Basic Usage:** +```java +// Find nearest player +Rs2PlayerModel player = new Rs2PlayerQueryable() + .nearest(); + +// Find player by name +Rs2PlayerModel target = new Rs2PlayerQueryable() + .withName("PlayerName") + .nearest(); + +// Find all friends nearby +List friends = new Rs2PlayerQueryable() + .where(Rs2PlayerModel::isFriend) + .within(20) + .toList(); +``` + +**Rs2PlayerModel Methods:** +```java +player.getName() // Player name +player.getCombatLevel() // Combat level +player.getHealthRatio() // Health (-1 if not visible) +player.isFriend() // Is friend? +player.isClanMember() // In your clan? +player.isFriendsChatMember() // In friends chat? +player.getSkullIcon() // Skull icon (-1 if none) +player.getOverheadIcon() // Prayer icon +player.getAnimation() // Current animation +player.isInteracting() // Is interacting? +``` + +### 4. Rs2TileObjectQueryable - Tile Object Queries + +**Import:** +```java +import net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectQueryable; +import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; +``` + +**Basic Usage:** +```java +// Find nearest tree +Rs2TileObjectModel tree = new Rs2TileObjectQueryable() + .withName("Tree") + .nearest(); + +// Find nearest bank booth +Rs2TileObjectModel bank = new Rs2TileObjectQueryable() + .withName("Bank booth") + .nearest(); + +// Find all rocks within 15 tiles +List rocks = new Rs2TileObjectQueryable() + .where(obj -> obj.getName() != null && + obj.getName().contains("rocks")) + .within(15) + .toList(); +``` + +**Rs2TileObjectModel Methods:** +```java +obj.getName() // "Tree" +obj.getId() // Object ID +obj.getWorldLocation() // WorldPoint +obj.click("Chop down") // Interact with object +obj.interact("Mine") // Alternative interact +``` + +--- + +## API Reference + +### Terminal Operations + +These execute the query and return results: + +#### `nearest()` +Returns the nearest entity to the player. + +```java +Rs2NpcModel npc = new Rs2NpcQueryable() + .withName("Guard") + .nearest(); +``` + +#### `nearest(int maxDistance)` +Returns the nearest entity within max distance from player. + +```java +Rs2NpcModel npc = new Rs2NpcQueryable() + .withName("Guard") + .nearest(10); // Within 10 tiles +``` + +#### `nearest(WorldPoint anchor, int maxDistance)` +Returns the nearest entity to a specific point. + +```java +WorldPoint location = new WorldPoint(3100, 3500, 0); +Rs2NpcModel npc = new Rs2NpcQueryable() + .withName("Guard") + .nearest(location, 5); +``` + +#### `first()` +Returns the first matching entity (not necessarily nearest). + +```java +Rs2NpcModel npc = new Rs2NpcQueryable() + .withName("Guard") + .first(); +``` + +#### `withName(String name)` +Finds nearest entity with exact name (case-insensitive). + +```java +Rs2NpcModel banker = new Rs2NpcQueryable() + .withName("Banker"); // Terminal operation +``` + +#### `withNames(String... names)` +Finds nearest entity matching any of the names. + +```java +Rs2NpcModel npc = new Rs2NpcQueryable() + .withNames("Banker", "Bank clerk", "Bank assistant"); +``` + +#### `withId(int id)` +Finds nearest entity with specific ID. + +```java +Rs2NpcModel npc = new Rs2NpcQueryable() + .withId(1234); +``` + +#### `withIds(int... ids)` +Finds nearest entity matching any of the IDs. + +```java +Rs2NpcModel npc = new Rs2NpcQueryable() + .withIds(1234, 5678, 9012); +``` + +#### `toList()` +Returns all matching entities as a list. + +```java +List guards = new Rs2NpcQueryable() + .withName("Guard") + .toList(); +``` + +### Filter Operations + +These filter entities and return the queryable for chaining: + +#### `where(Predicate predicate)` +Adds a custom filter using a lambda expression. + +```java +new Rs2NpcQueryable() + .where(npc -> npc.getHealthRatio() > 0) + .where(npc -> !npc.isInteracting()) + .nearest(); +``` + +#### `within(int distance)` +Filters entities within distance from player. + +```java +new Rs2NpcQueryable() + .withName("Guard") + .within(10) + .toList(); +``` + +#### `within(WorldPoint anchor, int distance)` +Filters entities within distance from a specific point. + +```java +WorldPoint location = new WorldPoint(3100, 3500, 0); +new Rs2NpcQueryable() + .withName("Guard") + .within(location, 15) + .toList(); +``` + +--- + +## Common Patterns + +### Pattern 1: Find Nearest Non-Interacting NPC + +```java +Rs2NpcModel cow = new Rs2NpcQueryable() + .withName("Cow") + .where(npc -> !npc.isInteracting()) + .nearest(); + +if (cow != null) { + cow.click("Attack"); +} +``` + +### Pattern 2: Find Valuable Loot + +```java +Rs2TileItemModel loot = new Rs2TileItemQueryable() + .where(item -> item.getTotalGeValue() >= 5000) + .where(Rs2TileItemModel::isLootAble) + .nearest(10); + +if (loot != null) { + loot.pickup(); +} +``` + +### Pattern 3: Find Multiple NPCs + +```java +List guards = new Rs2NpcQueryable() + .withName("Guard") + .where(npc -> !npc.isInteracting()) + .within(15) + .toList(); + +for (Rs2NpcModel guard : guards) { + // Do something with each guard +} +``` + +### Pattern 4: Find by Multiple Names + +```java +Rs2NpcModel banker = new Rs2NpcQueryable() + .withNames("Banker", "Bank clerk", "Bank assistant"); + +if (banker != null) { + banker.click("Bank"); +} +``` + +### Pattern 5: Complex Query with Multiple Filters + +```java +Rs2NpcModel target = new Rs2NpcQueryable() + .withName("Goblin") + .where(npc -> !npc.isInteracting()) + .where(npc -> npc.getHealthRatio() > 0) + .where(npc -> npc.getAnimation() == -1) // Not animating + .within(10) + .nearest(); +``` + +### Pattern 6: Find Nearest Object by Partial Name + +```java +Rs2TileObjectModel tree = new Rs2TileObjectQueryable() + .where(obj -> obj.getName() != null && + obj.getName().toLowerCase().contains("tree")) + .nearest(); +``` + +### Pattern 7: Find Items About to Despawn + +```java +List despawning = new Rs2TileItemQueryable() + .where(item -> item.willDespawnWithin(30)) // 30 seconds + .where(item -> item.getTotalValue() > 1000) + .toList(); +``` + +### Pattern 8: Find Friends Nearby + +```java +List friends = new Rs2PlayerQueryable() + .where(Rs2PlayerModel::isFriend) + .within(20) + .toList(); +``` + +### Pattern 9: Find Low Health Enemies + +```java +Rs2NpcModel weakEnemy = new Rs2NpcQueryable() + .withName("Goblin") + .where(npc -> npc.getHealthRatio() > 0 && + npc.getHealthRatio() < 10) // Low health + .nearest(); +``` + +### Pattern 10: Find Specific Object by ID + +```java +Rs2TileObjectModel altar = new Rs2TileObjectQueryable() + .withId(409) // Altar object ID + .nearest(); +``` + +--- + +## Advanced Usage + +### Custom Predicates + +Create reusable predicates for common filters: + +```java +// Define predicates +Predicate isAlive = npc -> npc.getHealthRatio() > 0; +Predicate notBusy = npc -> !npc.isInteracting(); +Predicate notAnimating = npc -> npc.getAnimation() == -1; + +// Use them +Rs2NpcModel target = new Rs2NpcQueryable() + .withName("Cow") + .where(isAlive) + .where(notBusy) + .where(notAnimating) + .nearest(); +``` + +### Combining Predicates + +```java +// Combine with AND +Predicate attackable = + npc -> !npc.isInteracting() && npc.getHealthRatio() > 0; + +Rs2NpcModel target = new Rs2NpcQueryable() + .withName("Goblin") + .where(attackable) + .nearest(); + +// Combine with OR +Predicate valuableOrStackable = + item -> item.getTotalValue() > 1000 || item.isStackable(); + +Rs2TileItemModel loot = new Rs2TileItemQueryable() + .where(valuableOrStackable) + .nearest(); +``` + +### Distance-Based Queries + +```java +// Find nearest NPC within specific range +WorldPoint homeBase = new WorldPoint(3100, 3500, 0); + +Rs2NpcModel nearbyEnemy = new Rs2NpcQueryable() + .withName("Goblin") + .within(homeBase, 10) // Within 10 tiles of home base + .nearest(homeBase, 10); // Get nearest to home base +``` + +### Sorting and Limiting + +```java +// Get 5 nearest guards +List guards = new Rs2NpcQueryable() + .withName("Guard") + .toList() + .stream() + .sorted(Comparator.comparingInt(npc -> + npc.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) + .limit(5) + .collect(Collectors.toList()); +``` + +### Null Safety + +Always check for null results: + +```java +Rs2NpcModel banker = new Rs2NpcQueryable() + .withName("Banker") + .nearest(); + +if (banker != null) { + banker.click("Bank"); +} else { + log.warn("No banker found nearby"); +} +``` + +### Using with sleepUntil + +```java +// Wait until target NPC appears +Rs2NpcModel target = sleepUntilNotNull(() -> + new Rs2NpcQueryable() + .withName("Banker") + .nearest(), + 5000, 600 // 5 second timeout, check every 600ms +); + +if (target != null) { + target.click("Bank"); +} +``` + +--- + +## Performance Tips + +### ✅ DO: + +**1. Use specific filters early:** +```java +// Good - filters by name first +new Rs2NpcQueryable() + .withName("Guard") + .where(npc -> !npc.isInteracting()) + .nearest(); +``` + +**2. Limit search radius:** +```java +// Good - only searches within 10 tiles +new Rs2NpcQueryable() + .withName("Guard") + .within(10) + .nearest(); +``` + +**3. Cache results when possible:** +```java +// Good - query once, use multiple times +List guards = new Rs2NpcQueryable() + .withName("Guard") + .toList(); + +for (Rs2NpcModel guard : guards) { + // Process each guard +} +``` + +**4. Use method references:** +```java +// Good - cleaner and potentially faster +new Rs2TileItemQueryable() + .where(Rs2TileItemModel::isLootAble) + .nearest(); +``` + +### ❌ DON'T: + +**1. Don't query in tight loops:** +```java +// Bad - queries on every iteration +while (true) { + Rs2NpcModel npc = new Rs2NpcQueryable() + .withName("Guard") + .nearest(); + // ... + sleep(100); // Still too frequent +} + +// Good - reasonable interval +while (true) { + Rs2NpcModel npc = new Rs2NpcQueryable() + .withName("Guard") + .nearest(); + // ... + sleep(600); // ~1 game tick +} +``` + +**2. Don't use expensive operations in predicates:** +```java +// Bad - calls API repeatedly in filter +new Rs2NpcQueryable() + .where(npc -> someExpensiveApiCall(npc)) + .nearest(); + +// Good - call API once, cache result +boolean shouldFilter = someExpensiveApiCall(); +new Rs2NpcQueryable() + .where(npc -> shouldFilter) + .nearest(); +``` + +**3. Don't create unnecessary lists:** +```java +// Bad - creates full list just to get one item +Rs2NpcModel npc = new Rs2NpcQueryable() + .withName("Guard") + .toList() + .get(0); + +// Good - gets first directly +Rs2NpcModel npc = new Rs2NpcQueryable() + .withName("Guard") + .first(); +``` + +--- + +## Migration Guide + +### From Legacy API to Queryable API + +#### NPCs + +**Legacy:** +```java +// Old way +NPC npc = Rs2Npc.getNpc("Banker"); +NPC nearest = Rs2Npc.getNearestNpc("Guard"); +List npcs = Rs2Npc.getNpcs(NpcID.GUARD); +``` + +**Queryable:** +```java +// New way +Rs2NpcModel npc = new Rs2NpcQueryable() + .withName("Banker"); + +Rs2NpcModel nearest = new Rs2NpcQueryable() + .withName("Guard") + .nearest(); + +List npcs = new Rs2NpcQueryable() + .withId(NpcID.GUARD) + .toList(); +``` + +#### Ground Items + +**Legacy:** +```java +// Old way +TileItem item = Rs2GroundItem.findItem("Coins"); +TileItem nearest = Rs2GroundItem.getNearestItem("Dragon bones"); +``` + +**Queryable:** +```java +// New way +Rs2TileItemModel item = new Rs2TileItemQueryable() + .withName("Coins") + .first(); + +Rs2TileItemModel nearest = new Rs2TileItemQueryable() + .withName("Dragon bones") + .nearest(); +``` + +#### Game Objects + +**Legacy:** +```java +// Old way +TileObject tree = Rs2GameObject.findObject("Tree"); +TileObject nearest = Rs2GameObject.findObjectById(1234); +``` + +**Queryable:** +```java +// New way +Rs2TileObjectModel tree = new Rs2TileObjectQueryable() + .withName("Tree") + .nearest(); + +Rs2TileObjectModel nearest = new Rs2TileObjectQueryable() + .withId(1234) + .nearest(); +``` + +### Step-by-Step Migration + +**Step 1:** Replace direct method calls with queryable: +```java +// Before +NPC banker = Rs2Npc.getNpc("Banker"); + +// After +Rs2NpcModel banker = new Rs2NpcQueryable().withName("Banker"); +``` + +**Step 2:** Add filters if needed: +```java +// Before +NPC cow = Rs2Npc.getNpcs().stream() + .filter(npc -> npc.getName().equals("Cow")) + .filter(npc -> npc.getInteracting() == null) + .findFirst() + .orElse(null); + +// After +Rs2NpcModel cow = new Rs2NpcQueryable() + .withName("Cow") + .where(npc -> !npc.isInteracting()) + .nearest(); +``` + +**Step 3:** Update interaction methods: +```java +// Before +if (banker != null) { + Rs2Npc.interact(banker, "Bank"); +} + +// After +if (banker != null) { + banker.click("Bank"); + // or + banker.interact("Bank"); +} +``` + +--- + +## Examples by Use Case + +### Combat Scripts + +```java +// Find nearest attackable enemy +Rs2NpcModel enemy = new Rs2NpcQueryable() + .withName("Goblin") + .where(npc -> !npc.isInteracting()) + .where(npc -> npc.getHealthRatio() > 0) + .nearest(10); + +if (enemy != null && !Rs2Player.isInCombat()) { + enemy.click("Attack"); + sleepUntil(() -> Rs2Player.isInCombat(), 2000); +} +``` + +### Looting Scripts + +```java +// Find valuable loot +Rs2TileItemModel loot = new Rs2TileItemQueryable() + .where(Rs2TileItemModel::isLootAble) + .where(item -> item.getTotalGeValue() >= 5000) + .nearest(15); + +if (loot != null) { + loot.pickup(); + sleepUntil(() -> Rs2Inventory.contains(loot.getName()), 3000); +} +``` + +### Skilling Scripts + +```java +// Find nearest available tree +Rs2TileObjectModel tree = new Rs2TileObjectQueryable() + .where(obj -> obj.getName() != null && + obj.getName().equals("Oak tree")) + .nearest(10); + +if (tree != null && !Rs2Player.isAnimating()) { + tree.click("Chop down"); + sleepUntil(() -> Rs2Player.isAnimating(), 3000); +} +``` + +### Banking Scripts + +```java +// Find nearest bank +Rs2TileObjectModel bank = new Rs2TileObjectQueryable() + .withNames("Bank booth", "Bank chest", "Bank") + .nearest(20); + +if (bank != null && !Rs2Bank.isOpen()) { + bank.click("Bank"); + sleepUntil(() -> Rs2Bank.isOpen(), 5000); +} +``` + +--- + +## Troubleshooting + +### Query Returns Null + +**Problem:** Query returns `null` even though entity exists. + +**Solutions:** + +1. **Check distance:** +```java +// Increase search radius +.nearest(20) // Instead of default +``` + +2. **Verify name:** +```java +// Names are case-insensitive but must be exact +.withName("Banker") // Not "banker" or "Bank" +``` + +3. **Check filters:** +```java +// Simplify query to find the issue +Rs2NpcModel test1 = new Rs2NpcQueryable().withName("Banker"); +Rs2NpcModel test2 = new Rs2NpcQueryable().withName("Banker").where(filter); +// If test1 works but test2 doesn't, your filter is too restrictive +``` + +4. **Verify entity is loaded:** +```java +// Wait for entity to appear +Rs2NpcModel npc = sleepUntilNotNull(() -> + new Rs2NpcQueryable().withName("Banker").nearest(), + 5000, 600 +); +``` + +### Performance Issues + +**Problem:** Queries are slow or cause lag. + +**Solutions:** + +1. **Limit search area:** +```java +.within(15) // Only search nearby +``` + +2. **Cache results:** +```java +// Query once per game tick, not every iteration +List npcs = new Rs2NpcQueryable() + .withName("Guard") + .toList(); +``` + +3. **Simplify predicates:** +```java +// Avoid complex calculations in where() clauses +.where(npc -> simpleCheck(npc)) // Good +.where(npc -> complexCalculation(npc)) // Bad +``` + +### Interaction Failures + +**Problem:** Entity found but click doesn't work. + +**Solutions:** + +1. **Check null:** +```java +Rs2NpcModel npc = new Rs2NpcQueryable().withName("Banker"); +if (npc != null) { // Always check + npc.click("Bank"); +} +``` + +2. **Wait for action:** +```java +npc.click("Bank"); +sleepUntil(() -> Rs2Bank.isOpen(), 5000); +``` + +3. **Verify entity still exists:** +```java +if (npc != null && npc.getWorldLocation() != null) { + npc.click("Bank"); +} +``` + +--- + +## Best Practices Summary + +✅ **DO:** +- Use queryable API for new code +- Chain filters for readability +- Check for null results +- Use `nearest()` for single entities +- Use `toList()` for multiple entities +- Cache query results when appropriate +- Use method references when possible +- Add distance limits to queries + +❌ **DON'T:** +- Query in tight loops without delays +- Use expensive operations in filters +- Forget null checks +- Create unnecessary intermediate lists +- Use legacy API for new code +- Query without distance limits + +--- + +## Quick Reference + +### Imports + +```java +// NPCs +import net.runelite.client.plugins.microbot.api.npc.Rs2NpcQueryable; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; + +// Ground Items +import net.runelite.client.plugins.microbot.api.tileitem.Rs2TileItemQueryable; +import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; + +// Players +import net.runelite.client.plugins.microbot.api.player.Rs2PlayerQueryable; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; + +// Tile Objects +import net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectQueryable; +import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; +``` + +### Quick Examples + +```java +// Find nearest NPC +new Rs2NpcQueryable().withName("Banker").nearest(); + +// Find ground item +new Rs2TileItemQueryable().withName("Coins").nearest(); + +// Find player +new Rs2PlayerQueryable().withName("PlayerName").nearest(); + +// Find object +new Rs2TileObjectQueryable().withName("Tree").nearest(); + +// Complex query +new Rs2NpcQueryable() + .withName("Guard") + .where(npc -> !npc.isInteracting()) + .within(10) + .nearest(); +``` + +--- + +## Additional Resources + +- **CLAUDE.md** - Full framework documentation +- **Example Scripts** - See `api/*/ApiExample.java` files +- **Discord** - https://discord.gg/zaGrfqFEWE +- **Website** - https://themicrobot.com + +--- + +**Last Updated:** November 18, 2025 +**Microbot Version:** 2.1.0 +**For questions or issues, please visit our Discord community.** + diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/README.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/README.md new file mode 100644 index 00000000000..2c9c099a729 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/README.md @@ -0,0 +1,145 @@ +# Microbot API Documentation + +This directory contains the new Queryable API for interacting with game entities. + +## 📚 Documentation + +- **[QUERYABLE_API.md](QUERYABLE_API.md)** - Complete guide to using the Queryable API + - Introduction and benefits + - Getting started guide + - API reference + - Common patterns and examples + - Performance tips + - Migration guide from legacy API + - Troubleshooting + +## 🚀 Quick Start + +### NPCs +```java +import net.runelite.client.plugins.microbot.api.npc.Rs2NpcQueryable; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; + +Rs2NpcModel banker = new Rs2NpcQueryable() + .withName("Banker") + .nearest(); +``` + +### Ground Items +```java +import net.runelite.client.plugins.microbot.api.tileitem.Rs2TileItemQueryable; +import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; + +Rs2TileItemModel coins = new Rs2TileItemQueryable() + .withName("Coins") + .nearest(); +``` + +### Players +```java +import net.runelite.client.plugins.microbot.api.player.Rs2PlayerQueryable; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; + +Rs2PlayerModel player = new Rs2PlayerQueryable() + .withName("PlayerName") + .nearest(); +``` + +### Tile Objects +```java +import net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectQueryable; +import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; + +Rs2TileObjectModel tree = new Rs2TileObjectQueryable() + .withName("Tree") + .nearest(); +``` + +## 📂 Directory Structure + +``` +api/ +├── README.md # This file +├── QUERYABLE_API.md # Complete API documentation +│ +├── IEntityQueryable.java # Generic queryable interface +├── AbstractEntityQueryable.java # Base implementation +├── IEntity.java # Base entity interface +│ +├── npc/ # NPC queries +│ ├── Rs2NpcQueryable.java +│ ├── Rs2NpcCache.java +│ └── models/ +│ └── Rs2NpcModel.java +│ +├── tileitem/ # Ground item queries +│ ├── Rs2TileItemQueryable.java +│ ├── Rs2TileItemCache.java +│ ├── TileItemApiExample.java +│ └── models/ +│ └── Rs2TileItemModel.java +│ +├── player/ # Player queries +│ ├── Rs2PlayerQueryable.java +│ ├── Rs2PlayerCache.java +│ ├── PlayerApiExample.java +│ └── models/ +│ └── Rs2PlayerModel.java +│ +├── tileobject/ # Tile object queries +│ ├── Rs2TileObjectQueryable.java +│ ├── Rs2TileObjectCache.java +│ └── models/ +│ └── Rs2TileObjectModel.java +│ +├── actor/ # Actor utilities +└── playerstate/ # Player state cache +``` + +## 🔥 Why Use Queryable API? + +### Before (Legacy) ❌ +```java +NPC banker = null; +for (NPC npc : client.getNpcs()) { + if (npc.getName() != null && npc.getName().equals("Banker")) { + if (banker == null || npc.getWorldLocation().distanceTo(player.getWorldLocation()) + < banker.getWorldLocation().distanceTo(player.getWorldLocation())) { + banker = npc; + } + } +} +``` + +### After (Queryable) ✅ +```java +Rs2NpcModel banker = new Rs2NpcQueryable() + .withName("Banker") + .nearest(); +``` + +**Benefits:** +- 📖 More readable and maintainable +- 🚀 Faster development +- 🐛 Fewer bugs (type-safe) +- ⚡ Better performance (optimized internally) + +## 📖 Examples + +Check the `*ApiExample.java` files in each subdirectory for complete examples: + +- `npc/NpcApiExample.java` - NPC query examples +- `tileitem/TileItemApiExample.java` - Ground item examples +- `player/PlayerApiExample.java` - Player query examples + +## 🔗 Additional Resources + +- **Main Documentation**: `../CLAUDE.md` +- **Discord**: https://discord.gg/zaGrfqFEWE +- **Website**: https://themicrobot.com + +--- + +**Last Updated:** November 18, 2025 +**Microbot Version:** 2.1.0 + diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModel.java new file mode 100644 index 00000000000..8a04bbabc1f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModel.java @@ -0,0 +1,470 @@ +package net.runelite.client.plugins.microbot.api.actor; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Point; +import net.runelite.api.*; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.IEntity; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.awt.image.BufferedImage; + +@Getter +@RequiredArgsConstructor +public class Rs2ActorModel implements Actor +{ + + private final Actor actor; + + @Override + public WorldView getWorldView() + { + return Microbot.getClientThread().invoke(actor::getWorldView); + } + + @Override + public LocalPoint getCameraFocus() { + return Microbot.getClientThread().runOnClientThreadOptional(actor::getCameraFocus).orElse(null); + } + + @Override + public int getCombatLevel() + { + return Microbot.getClientThread().runOnClientThreadOptional(actor::getCombatLevel).orElse(0); + } + + @Override + public @Nullable String getName() + { + return Microbot.getClientThread().runOnClientThreadOptional(actor::getName).orElse(null); + } + + @Override + public boolean isInteracting() + { + return actor.isInteracting(); + } + + @Override + public Actor getInteracting() + { + return Microbot.getClientThread().runOnClientThreadOptional(actor::getInteracting).orElse(null); + } + + @Override + public int getHealthRatio() + { + return Microbot.getClientThread().runOnClientThreadOptional(actor::getHealthRatio).orElse(0); + } + + @Override + public int getHealthScale() + { + return Microbot.getClientThread().runOnClientThreadOptional(actor::getHealthScale).orElse(0); + } + + @Override + public WorldPoint getWorldLocation() + { + if (getWorldView() != null && getWorldView().getId() != -1) { + return Microbot.getClientThread().invoke(this::projectActorLocationToMainWorld); + } + + return actor.getWorldLocation(); + } + + @Override + public LocalPoint getLocalLocation() + { + return actor.getLocalLocation(); + } + + @Override + public int getOrientation() + { + return actor.getOrientation(); + } + + @Override + public int getCurrentOrientation() + { + return actor.getCurrentOrientation(); + } + + @Override + public int getAnimation() + { + return actor.getAnimation(); + } + + @Override + public int getPoseAnimation() + { + return actor.getPoseAnimation(); + } + + @Override + public void setPoseAnimation(int animation) + { + actor.setPoseAnimation(animation); + } + + @Override + public int getPoseAnimationFrame() + { + return actor.getPoseAnimationFrame(); + } + + @Override + public void setPoseAnimationFrame(int frame) + { + actor.setPoseAnimationFrame(frame); + } + + @Override + public int getIdlePoseAnimation() + { + return actor.getIdlePoseAnimation(); + } + + @Override + public void setIdlePoseAnimation(int animation) + { + actor.setIdlePoseAnimation(animation); + } + + @Override + public int getIdleRotateLeft() + { + return actor.getIdleRotateLeft(); + } + + @Override + public void setIdleRotateLeft(int animationID) + { + actor.setIdleRotateLeft(animationID); + } + + @Override + public int getIdleRotateRight() + { + return actor.getIdleRotateRight(); + } + + @Override + public void setIdleRotateRight(int animationID) + { + actor.setIdleRotateRight(animationID); + } + + @Override + public int getWalkAnimation() + { + return actor.getWalkAnimation(); + } + + @Override + public void setWalkAnimation(int animationID) + { + actor.setWalkAnimation(animationID); + } + + @Override + public int getWalkRotateLeft() + { + return actor.getWalkRotateLeft(); + } + + @Override + public void setWalkRotateLeft(int animationID) + { + actor.setWalkRotateLeft(animationID); + } + + @Override + public int getWalkRotateRight() + { + return actor.getWalkRotateRight(); + } + + @Override + public void setWalkRotateRight(int animationID) + { + actor.setWalkRotateRight(animationID); + } + + @Override + public int getWalkRotate180() + { + return actor.getWalkRotate180(); + } + + @Override + public void setWalkRotate180(int animationID) + { + actor.setWalkRotate180(animationID); + } + + @Override + public int getRunAnimation() + { + return actor.getRunAnimation(); + } + + @Override + public void setRunAnimation(int animationID) + { + actor.setRunAnimation(animationID); + } + + @Override + public void setAnimation(int animation) + { + actor.setAnimation(animation); + } + + @Override + public int getAnimationFrame() + { + return actor.getAnimationFrame(); + } + + @Override + public void setActionFrame(int frame) + { + actor.setAnimationFrame(frame); + } + + @Override + public void setAnimationFrame(int frame) + { + actor.setAnimationFrame(frame); + } + + @Override + public IterableHashTable getSpotAnims() + { + return actor.getSpotAnims(); + } + + @Override + public boolean hasSpotAnim(int spotAnimId) + { + return actor.hasSpotAnim(spotAnimId); + } + + @Override + public void createSpotAnim(int id, int spotAnimId, int height, int delay) + { + actor.createSpotAnim(id, spotAnimId, height, delay); + } + + @Override + public void removeSpotAnim(int id) + { + actor.removeSpotAnim(id); + } + + @Override + public void clearSpotAnims() + { + actor.clearSpotAnims(); + } + + @Override + public int getGraphic() + { + return actor.getGraphic(); + } + + @Override + public void setGraphic(int graphic) + { + actor.setGraphic(graphic); + } + + @Override + public int getGraphicHeight() + { + return actor.getGraphicHeight(); + } + + @Override + public void setGraphicHeight(int height) + { + actor.setGraphicHeight(height); + } + + @Override + public int getSpotAnimFrame() + { + return actor.getSpotAnimFrame(); + } + + @Override + public void setSpotAnimFrame(int spotAnimFrame) + { + actor.setSpotAnimFrame(spotAnimFrame); + } + + @Override + public Polygon getCanvasTilePoly() + { + return actor.getCanvasTilePoly(); + } + + @Override + public @Nullable Point getCanvasTextLocation(Graphics2D graphics, String text, int zOffset) + { + return actor.getCanvasTextLocation(graphics, text, zOffset); + } + + @Override + public Point getCanvasImageLocation(BufferedImage image, int zOffset) + { + return actor.getCanvasImageLocation(image, zOffset); + } + + @Override + public Point getCanvasSpriteLocation(SpritePixels sprite, int zOffset) + { + return actor.getCanvasSpriteLocation(sprite, zOffset); + } + + @Override + public Point getMinimapLocation() + { + return actor.getMinimapLocation(); + } + + @Override + public int getLogicalHeight() + { + return actor.getLogicalHeight(); + } + + @Override + public Shape getConvexHull() + { + return actor.getConvexHull(); + } + + @Override + public WorldArea getWorldArea() + { + return actor.getWorldArea(); + } + + @Override + public String getOverheadText() + { + return actor.getOverheadText(); + } + + @Override + public void setOverheadText(String overheadText) + { + actor.setOverheadText(overheadText); + } + + @Override + public int getOverheadCycle() + { + return actor.getOverheadCycle(); + } + + @Override + public void setOverheadCycle(int cycles) + { + actor.setOverheadCycle(cycles); + } + + @Override + public boolean isDead() + { + return actor.isDead(); + } + + @Override + public void setDead(boolean dead) + { + actor.setDead(dead); + } + + @Override + public int getAnimationHeightOffset() + { + return actor.getAnimationHeightOffset(); + } + + @Override + public Model getModel() + { + return actor.getModel(); + } + + @Override + public int getModelHeight() + { + return actor.getModelHeight(); + } + + @Override + public void setModelHeight(int modelHeight) + { + actor.setModelHeight(modelHeight); + } + + @Override + public Node getNext() + { + return actor.getNext(); + } + + @Override + public Node getPrevious() + { + return actor.getPrevious(); + } + + @Override + public long getHash() + { + return actor.getHash(); + } + + public WorldPoint projectActorLocationToMainWorld() { + WorldPoint actorLocation = actor.getWorldLocation(); + LocalPoint localPoint = LocalPoint.fromWorld( + getWorldView(), + actorLocation + ); + + if (localPoint == null) + { + return actorLocation; + } + + var mainWorldProjection = getWorldView().getMainWorldProjection(); + + if (mainWorldProjection == null) + { + return actorLocation; + } + + float[] projection = mainWorldProjection + .project(localPoint.getX(), 0, localPoint.getY()); + + return WorldPoint.fromLocal( + Microbot.getClient().getTopLevelWorldView(), + (int) projection[0], + (int) projection[2], + 0 + ); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/Rs2BoatCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/Rs2BoatCache.java new file mode 100644 index 00000000000..7a21dfe22a4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/Rs2BoatCache.java @@ -0,0 +1,91 @@ +package net.runelite.client.plugins.microbot.api.boat; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.events.ChatMessage; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.boat.models.Rs2BoatModel; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +@Slf4j +public final class Rs2BoatCache +{ + private int lastCheckedOnBoat = 0; + private WorldEntity boat = null; + + + public Rs2BoatCache() + { + } + + public Rs2BoatModel getLocalBoat() + { + if (lastCheckedOnBoat * 2 >= Microbot.getClient().getTickCount()) { + return new Rs2BoatModel(boat); + } + + boat = Microbot.getClientThread().invoke(() -> + { + lastCheckedOnBoat = Microbot.getClient().getTickCount(); + Client client = Microbot.getClient(); + Player player = client.getLocalPlayer(); + + if (player == null) + { + return null; + } + + WorldView playerView = player.getWorldView(); + + if (!playerView.isTopLevel()) + { + LocalPoint playerLocal = player.getLocalLocation(); + int worldViewId = playerLocal.getWorldView(); + + return client.getTopLevelWorldView() + .worldEntities() + .byIndex(worldViewId); + } + + return null; + }); + + return new Rs2BoatModel(boat); + } + + public Rs2BoatModel getBoat(Rs2PlayerModel player) + { + if (lastCheckedOnBoat * 2 >= Microbot.getClient().getTickCount()) { + return new Rs2BoatModel(boat); + } + + boat = Microbot.getClientThread().invoke(() -> + { + lastCheckedOnBoat = Microbot.getClient().getTickCount(); + Client client = Microbot.getClient(); + + if (player == null) + { + return null; + } + + WorldView playerView = player.getWorldView(); + + if (!playerView.isTopLevel()) + { + LocalPoint playerLocal = player.getLocalLocation(); + int worldViewId = playerLocal.getWorldView(); + + return client.getTopLevelWorldView() + .worldEntities() + .byIndex(worldViewId); + } + + return null; + }); + + return new Rs2BoatModel(boat); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/BoatType.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/BoatType.java new file mode 100644 index 00000000000..bb81cac212b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/BoatType.java @@ -0,0 +1,7 @@ +package net.runelite.client.plugins.microbot.api.boat.data; + +public enum BoatType { + RAFT, + SKIFF, + SLOOP, +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/Heading.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/Heading.java similarity index 91% rename from runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/Heading.java rename to runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/Heading.java index c3905e1477f..575c0057b08 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/Heading.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/Heading.java @@ -1,4 +1,4 @@ -package net.runelite.client.plugins.microbot.util.sailing.data; +package net.runelite.client.plugins.microbot.api.boat.data; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/LedgerID.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/LedgerID.java similarity index 98% rename from runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/LedgerID.java rename to runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/LedgerID.java index 6253e258097..131b07f1f8d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/LedgerID.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/LedgerID.java @@ -1,4 +1,4 @@ -package net.runelite.client.plugins.microbot.util.sailing.data; +package net.runelite.client.plugins.microbot.api.boat.data; import net.runelite.api.gameval.ObjectID; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/PortLocation.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/PortLocation.java similarity index 98% rename from runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/PortLocation.java rename to runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/PortLocation.java index 95dc5dd7a89..3a11fddfb56 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/PortLocation.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/PortLocation.java @@ -1,4 +1,4 @@ -package net.runelite.client.plugins.microbot.util.sailing.data; +package net.runelite.client.plugins.microbot.api.boat.data; import java.util.Collections; import java.util.HashSet; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/PortPaths.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/PortPaths.java similarity index 99% rename from runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/PortPaths.java rename to runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/PortPaths.java index bf44d726a1b..bb586b0c9e4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/PortPaths.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/PortPaths.java @@ -1,4 +1,4 @@ -package net.runelite.client.plugins.microbot.util.sailing.data; +package net.runelite.client.plugins.microbot.api.boat.data; import java.util.ArrayList; import java.util.Collections; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/PortTaskData.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/PortTaskData.java similarity index 99% rename from runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/PortTaskData.java rename to runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/PortTaskData.java index 54bc53d2268..c7bc4dffdb1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/PortTaskData.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/PortTaskData.java @@ -1,4 +1,4 @@ -package net.runelite.client.plugins.microbot.util.sailing.data; +package net.runelite.client.plugins.microbot.api.boat.data; import lombok.Getter; import net.runelite.api.gameval.ItemID; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/PortTaskVarbits.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/PortTaskVarbits.java similarity index 97% rename from runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/PortTaskVarbits.java rename to runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/PortTaskVarbits.java index 9b734592df1..3e0b815a43c 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/PortTaskVarbits.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/PortTaskVarbits.java @@ -1,4 +1,4 @@ -package net.runelite.client.plugins.microbot.util.sailing.data; +package net.runelite.client.plugins.microbot.api.boat.data; import java.util.HashMap; import java.util.Map; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/RelativeMove.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/RelativeMove.java similarity index 92% rename from runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/RelativeMove.java rename to runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/RelativeMove.java index 2107794a950..aa31d286413 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/RelativeMove.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/RelativeMove.java @@ -1,4 +1,4 @@ -package net.runelite.client.plugins.microbot.util.sailing.data; +package net.runelite.client.plugins.microbot.api.boat.data; public final class RelativeMove { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/models/Rs2BoatModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/models/Rs2BoatModel.java new file mode 100644 index 00000000000..0267669a4f5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/models/Rs2BoatModel.java @@ -0,0 +1,692 @@ +package net.runelite.client.plugins.microbot.api.boat.models; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.InterfaceID; +import net.runelite.api.gameval.VarPlayerID; +import net.runelite.api.gameval.VarbitID; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.IEntity; +import net.runelite.client.plugins.microbot.api.boat.Rs2BoatCache; +import net.runelite.client.plugins.microbot.api.boat.data.BoatType; +import net.runelite.client.plugins.microbot.api.boat.data.Heading; +import net.runelite.client.plugins.microbot.api.boat.data.PortTaskData; +import net.runelite.client.plugins.microbot.api.boat.data.PortTaskVarbits; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; +import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; +import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; +import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +import static net.runelite.api.gameval.AnimationID.*; +import static net.runelite.api.gameval.ObjectID1.*; +import static net.runelite.client.plugins.microbot.util.Global.sleep; +import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; + +@Slf4j +public class Rs2BoatModel implements WorldEntity, IEntity { + + protected final WorldEntity boat; + + + public Rs2BoatModel(WorldEntity boat) + { + this.boat = boat; + } + + // Temp fix for disembark plank ids + public final int[] GANGPLANK_DISEMBARK = { + SAILING_GANGPLANK_PORT_SARIM, + SAILING_GANGPLANK_THE_PANDEMONIUM, + SAILING_GANGPLANK_LANDS_END, + SAILING_GANGPLANK_MUSA_POINT, + SAILING_GANGPLANK_HOSIDIUS, + SAILING_GANGPLANK_RIMMINGTON, + SAILING_GANGPLANK_CATHERBY, + SAILING_GANGPLANK_PORT_PISCARILIUS, + SAILING_GANGPLANK_BRIMHAVEN, + SAILING_GANGPLANK_ARDOUGNE, + SAILING_GANGPLANK_PORT_KHAZARD, + SAILING_GANGPLANK_WITCHAVEN, + SAILING_GANGPLANK_ENTRANA, + SAILING_GANGPLANK_CIVITAS_ILLA_FORTIS, + SAILING_GANGPLANK_CORSAIR_COVE, + SAILING_GANGPLANK_CAIRN_ISLE, + SAILING_GANGPLANK_SUNSET_COAST, + SAILING_GANGPLANK_THE_SUMMER_SHORE, + SAILING_GANGPLANK_ALDARIN, + SAILING_GANGPLANK_RUINS_OF_UNKAH, + SAILING_GANGPLANK_VOID_KNIGHTS_OUTPOST, + SAILING_GANGPLANK_PORT_ROBERTS, + SAILING_GANGPLANK_RED_ROCK, + SAILING_GANGPLANK_RELLEKKA, + SAILING_GANGPLANK_ETCETERIA, + SAILING_GANGPLANK_PORT_TYRAS, + SAILING_GANGPLANK_DEEPFIN_POINT, + SAILING_GANGPLANK_JATIZSO, + SAILING_GANGPLANK_NEITIZNOT, + SAILING_GANGPLANK_PRIFDDINAS, + SAILING_GANGPLANK_PISCATORIS, + SAILING_GANGPLANK_LUNAR_ISLE, + SAILING_MOORING_ISLE_OF_SOULS, + SAILING_MOORING_WATERBIRTH_ISLAND, + SAILING_MOORING_WEISS, + SAILING_MOORING_DOGNOSE_ISLAND, + SAILING_MOORING_REMOTE_ISLAND, + SAILING_MOORING_THE_LITTLE_PEARL, + SAILING_MOORING_THE_ONYX_CREST, + SAILING_MOORING_LAST_LIGHT, + SAILING_MOORING_CHARRED_ISLAND, + SAILING_MOORING_VATRACHOS_ISLAND, + SAILING_MOORING_ANGLERS_RETREAT, + SAILING_MOORING_MINOTAURS_REST, + SAILING_MOORING_ISLE_OF_BONES, + SAILING_MOORING_TEAR_OF_THE_SOUL, + SAILING_MOORING_WINTUMBER_ISLAND, + SAILING_MOORING_THE_CROWN_JEWEL, + SAILING_MOORING_RAINBOWS_END, + SAILING_MOORING_SUNBLEAK_ISLAND, + SAILING_MOORING_SHIMMERING_ATOLL, + SAILING_MOORING_LAGUNA_AURORAE, + SAILING_MOORING_CHINCHOMPA_ISLAND, + SAILING_MOORING_LLEDRITH_ISLAND, + SAILING_MOORING_YNYSDAIL, + SAILING_MOORING_BUCCANEERS_HAVEN, + SAILING_MOORING_DRUMSTICK_ISLE, + SAILING_MOORING_BRITTLE_ISLE, + SAILING_MOORING_GRIMSTONE + }; + + private Heading currentHeading = Heading.SOUTH; + + + @Override + public WorldView getWorldView() + { + return Microbot.getClientThread().runOnClientThreadOptional(boat::getWorldView).orElse(null); + } + + @Override + public LocalPoint getCameraFocus() + { + return Microbot.getClientThread().runOnClientThreadOptional(boat::getCameraFocus).orElse(null); + } + + @Override + public LocalPoint getLocalLocation() + { + return Microbot.getClientThread().runOnClientThreadOptional(boat::getLocalLocation).orElse(null); + } + + @Override + public WorldPoint getWorldLocation() + { + return Microbot.getClientThread().runOnClientThreadOptional(() -> + { + WorldView worldView = boat.getWorldView(); + LocalPoint localLocation = boat.getLocalLocation(); + + if (worldView == null || localLocation == null) + { + return null; + } + + return WorldPoint.fromLocal(worldView, localLocation.getX(), localLocation.getY(), worldView.getPlane()); + }).orElse(null); + } + + @Override + public int getOrientation() + { + return Microbot.getClientThread().runOnClientThreadOptional(boat::getOrientation).orElse(0); + } + + @Override + public LocalPoint getTargetLocation() + { + return Microbot.getClientThread().runOnClientThreadOptional(boat::getTargetLocation).orElse(null); + } + + @Override + public int getTargetOrientation() + { + return Microbot.getClientThread().runOnClientThreadOptional(boat::getTargetOrientation).orElse(0); + } + + @Override + public LocalPoint transformToMainWorld(LocalPoint point) + { + return Microbot.getClientThread().runOnClientThreadOptional(() -> boat.transformToMainWorld(point)).orElse(null); + } + + @Override + public boolean isHiddenForOverlap() + { + return Microbot.getClientThread().runOnClientThreadOptional(boat::isHiddenForOverlap).orElse(false); + } + + @Override + public WorldEntityConfig getConfig() + { + return Microbot.getClientThread().runOnClientThreadOptional(boat::getConfig).orElse(null); + } + + @Override + public int getOwnerType() + { + return Microbot.getClientThread().runOnClientThreadOptional(boat::getOwnerType).orElse(OWNER_TYPE_NOT_PLAYER); + } + + @Override + public int getId() + { + WorldEntityConfig config = getConfig(); + return config != null ? config.getId() : -1; + } + + @Override + public String getName() + { + return "WorldEntity"; + } + + @Override + public boolean click() + { + return click(""); + } + + @Override + public boolean click(String action) + { + return false; + } + + public BoatType getBoatType() + { + if (Microbot.getVarbitPlayerValue(VarPlayerID.SAILING_SIDEPANEL_BOAT_TYPE) == 8110) + { + return BoatType.RAFT; + } + return BoatType.RAFT; + } + + public int getSteeringForBoatType() + { + switch (getBoatType()) + { + case RAFT: + return SAILING_BOAT_STEERING_KANDARIN_1X3_WOOD; + case SKIFF: + // Return SKIFF steering object ID + case SLOOP: + // Return SLOOP steering object ID + default: + return SAILING_BOAT_STEERING_KANDARIN_1X3_WOOD; + } + } + + public boolean isOnBoat() + { + return boat != null; + } + + public boolean isNavigating() + { + return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_PLAYER_AT_HELM) == 1; + } + + public boolean navigate() + { + if (!isOnBoat()) + { + return false; + } + + if (isNavigating()) + { + return true; + } + + Rs2GameObject.interact(getSteeringForBoatType(), "Navigate"); + sleepUntil(() -> isNavigating(), 5000); + return isNavigating(); + } + + public WorldPoint getPlayerBoatLocation() + { + if (boat == null) + { + return null; + } + + return Microbot.getClientThread().invoke(() -> + { + Player player = Microbot.getClient().getLocalPlayer(); + if (player == null) + { + return null; + } + + WorldPoint playerLocation = player.getWorldLocation(); + LocalPoint localPoint = LocalPoint.fromWorld( + player.getWorldView(), + playerLocation + ); + + var mainWorldProjection = player + .getWorldView() + .getMainWorldProjection(); + + if (mainWorldProjection == null) + { + return playerLocation; + } + + float[] projection = mainWorldProjection + .project(localPoint.getX(), 0, localPoint.getY()); + + return Microbot.getClientThread().invoke(() -> WorldPoint.fromLocal( + Microbot.getClient().getTopLevelWorldView(), + (int) projection[0], + (int) projection[2], + 0 + )); + }); + } + + public boolean boardBoat() + { + if (isOnBoat()) + { + return true; + } + + int[] SAILING_GANGPLANKS = { + 59831, + 59832, + 59833, + 59834, + 59835, + 59836, + 59837, + 59838, + 59839, + 59840, + 59841, + 59842, + 59843, + 59844, + 59845, + 59846, + 59847, + 59848, + 59849, + 59850, + 59851, + 59852, + 59853, + 59854, + 59855, + 59856, + 59857, + 59858, + 59859, + 59860, + 59861, + 59862, + 59863, + 59864, + 59865, + 59866 + }; + Rs2GameObject.interact(SAILING_GANGPLANKS, "Board"); + sleepUntil(() -> isOnBoat(), 5000); + return isOnBoat(); + } + + public boolean disembarkBoat() + { + if (!isOnBoat()) + { + return true; + } + int[] SAILING_GANGPLANKS = { + 59831, + 59832, + 59833, + 59834, + 59835, + 59836, + 59837, + 59838, + 59839, + 59840, + 59841, + 59842, + 59843, + 59844, + 59845, + 59846, + 59847, + 59848, + 59849, + 59850, + 59851, + 59852, + 59853, + 59854, + 59855, + 59856, + 59857, + 59858, + 59859, + 59860, + 59861, + 59862, + 59863, + 59864, + 59865, + 59866 + }; + WorldView wv = Microbot.getClient().getTopLevelWorldView(); + + Scene scene = wv.getScene(); + Tile[][][] tiles = scene.getTiles(); + + int z = wv.getPlane(); + + for (int x = 0; x < tiles[z].length; ++x) + { + for (int y = 0; y < tiles[z][x].length; ++y) + { + Tile tile = tiles[z][x][y]; + + if (tile == null) + { + continue; + } + + Player player = Microbot.getClient().getLocalPlayer(); + if (player == null) + { + continue; + } + + if (tile.getGroundObject() == null) + { + continue; + } + + if (Arrays.stream(SAILING_GANGPLANKS).anyMatch(id -> id == tile.getGroundObject().getId())) + { + Rs2GameObject.clickObject(tile.getGroundObject(), "Disembark"); + sleepUntil(() -> !isOnBoat(), 5000); + } + } + } + Rs2GameObject.interact(SAILING_GANGPLANKS, "disembark"); + sleepUntil(() -> !isOnBoat(), 5000); + return !isOnBoat(); + } + + public boolean isMovingForward() + { + final int movingForward = 2; + return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_MOVE_MODE) == movingForward; + } + + public boolean isMovingBackward() + { + final int movingBackward = 3; + return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_MOVE_MODE) == movingBackward; + } + + public boolean isStandingStill() + { + final int standingStill = 0; + return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_MOVE_MODE) == standingStill; + } + + public boolean clickSailButton() + { + var widget = Rs2Widget.getWidget(InterfaceID.SailingSidepanel.FACILITIES_ROWS); + var setSailButton = widget.getDynamicChildren()[0]; + return Rs2Widget.clickWidget(setSailButton); + } + + public void setSails() + { + if (!isNavigating()) + { + return; + } + Rs2Tab.switchTo(InterfaceTab.COMBAT); + if (!isMovingForward()) + { + clickSailButton(); + sleepUntil(() -> isMovingForward(), 2500); + } + } + + public void unsetSails() + { + if (!isNavigating()) + { + return; + } + Rs2Tab.switchTo(InterfaceTab.COMBAT); + if (!isStandingStill()) + { + clickSailButton(); + sleepUntil(() -> isStandingStill(), 2500); + } + } + + public void sailTo(WorldPoint target) + { + if (!isOnBoat()) + { + var result = boardBoat(); + if (!result) + { + log.info("Failed to board boat."); + } + return; + } + if (!isNavigating()) + { + var result = navigate(); + if (!result) + { + log.info("Failed to navigate boat."); + } + return; + } + + var direction = getDirection(target); + var heading = Heading.getHeading(direction); + setHeading(heading); + + if (!isMovingForward()) + { + setSails(); + } + } + + public int getDirection(WorldPoint target) + { + double angle = getAngle(target); + + double rotated = 270.0 - angle; + + rotated %= 360.0; + if (rotated < 0) + { + rotated += 360.0; + } + + return (int) Math.round(rotated / 22.5) & 0xF; + } + + private double getAngle(WorldPoint target) + { + WorldPoint worldPoint = getWorldLocation(); + int playerX = worldPoint.getX(); + int playerY = worldPoint.getY(); + + int targetX = target.getX(); + int targetY = target.getY(); + + double dx = targetX - playerX; + double dy = targetY - playerY; + + return Math.toDegrees(Math.atan2(dy, dx)); + } + + public void setHeading(Heading heading) + { + if (heading == currentHeading) + { + return; + } + currentHeading = heading; + var menuEntry = new NewMenuEntry() + .option("Set-Heading") + .target("") + .identifier(heading.getValue()) + .type(MenuAction.SET_HEADING) + .param0(0) + .param1(0) + .forceLeftClick(false); + var worldview = Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldView()); + + if (worldview == null) + { + menuEntry.setWorldViewId(Microbot.getClient().getTopLevelWorldView().getId()); + } + else + { + menuEntry.setWorldViewId(worldview.getId()); + } + Microbot.doInvoke(menuEntry, new java.awt.Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + } + + public boolean trimSails() + { + sleep(2500, 3500); + if (!isOnBoat()) + { + return false; + } + final int[] SAIL_IDS = { + SAILING_BOAT_SAIL_KANDARIN_1X3_WOOD, + SAILING_BOAT_SAIL_KANDARIN_1X3_OAK, + SAILING_BOAT_SAIL_KANDARIN_1X3_TEAK, + SAILING_BOAT_SAIL_KANDARIN_1X3_MAHOGANY, + SAILING_BOAT_SAIL_KANDARIN_1X3_CAMPHOR, + SAILING_BOAT_SAIL_KANDARIN_1X3_IRONWOOD, + SAILING_BOAT_SAIL_KANDARIN_1X3_ROSEWOOD, + SAILING_BOAT_SAIL_KANDARIN_2X5_WOOD, + SAILING_BOAT_SAIL_KANDARIN_2X5_OAK, + SAILING_BOAT_SAIL_KANDARIN_2X5_TEAK, + SAILING_BOAT_SAIL_KANDARIN_2X5_MAHOGANY, + SAILING_BOAT_SAIL_KANDARIN_2X5_CAMPHOR, + SAILING_BOAT_SAIL_KANDARIN_2X5_IRONWOOD, + SAILING_BOAT_SAIL_KANDARIN_2X5_ROSEWOOD, + SAILING_BOAT_SAIL_KANDARIN_3X8_WOOD, + SAILING_BOAT_SAIL_KANDARIN_3X8_OAK, + SAILING_BOAT_SAIL_KANDARIN_3X8_TEAK, + SAILING_BOAT_SAIL_KANDARIN_3X8_MAHOGANY, + SAILING_BOAT_SAIL_KANDARIN_3X8_CAMPHOR, + SAILING_BOAT_SAIL_KANDARIN_3X8_IRONWOOD, + SAILING_BOAT_SAIL_KANDARIN_3X8_ROSEWOOD, + SAILING_BOAT_SAILS_COLOSSAL_REGULAR + }; + Rs2GameObject.interact(SAIL_IDS, "trim"); + return sleepUntil(() -> Microbot.isGainingExp, 5000); + } + + public boolean openCargo() + { + if (!isOnBoat()) + { + return false; + } + final int[] SAILING_BOAT_CARGO_HOLDS = { + SAILING_BOAT_CARGO_HOLD_REGULAR_RAFT, + SAILING_BOAT_CARGO_HOLD_REGULAR_RAFT_OPEN, + SAILING_BOAT_CARGO_HOLD_OAK_RAFT, + SAILING_BOAT_CARGO_HOLD_OAK_RAFT_OPEN, + SAILING_BOAT_CARGO_HOLD_TEAK_RAFT, + SAILING_BOAT_CARGO_HOLD_TEAK_RAFT_OPEN, + SAILING_BOAT_CARGO_HOLD_MAHOGANY_RAFT, + SAILING_BOAT_CARGO_HOLD_MAHOGANY_RAFT_OPEN, + SAILING_BOAT_CARGO_HOLD_CAMPHOR_RAFT, + SAILING_BOAT_CARGO_HOLD_CAMPHOR_RAFT_OPEN, + SAILING_BOAT_CARGO_HOLD_IRONWOOD_RAFT, + SAILING_BOAT_CARGO_HOLD_IRONWOOD_RAFT_OPEN, + SAILING_BOAT_CARGO_HOLD_ROSEWOOD_RAFT, + SAILING_BOAT_CARGO_HOLD_ROSEWOOD_RAFT_OPEN, + SAILING_BOAT_CARGO_HOLD_REGULAR_2X5, + SAILING_BOAT_CARGO_HOLD_REGULAR_2X5_OPEN, + SAILING_BOAT_CARGO_HOLD_OAK_2X5, + SAILING_BOAT_CARGO_HOLD_OAK_2X5_OPEN, + SAILING_BOAT_CARGO_HOLD_TEAK_2X5, + SAILING_BOAT_CARGO_HOLD_TEAK_2X5_OPEN, + SAILING_BOAT_CARGO_HOLD_MAHOGANY_2X5, + SAILING_BOAT_CARGO_HOLD_MAHOGANY_2X5_OPEN, + SAILING_BOAT_CARGO_HOLD_CAMPHOR_2X5, + SAILING_BOAT_CARGO_HOLD_CAMPHOR_2X5_OPEN, + SAILING_BOAT_CARGO_HOLD_IRONWOOD_2X5, + SAILING_BOAT_CARGO_HOLD_IRONWOOD_2X5_OPEN, + SAILING_BOAT_CARGO_HOLD_ROSEWOOD_2X5, + SAILING_BOAT_CARGO_HOLD_ROSEWOOD_2X5_OPEN, + SAILING_BOAT_CARGO_HOLD_REGULAR_LARGE, + SAILING_BOAT_CARGO_HOLD_REGULAR_LARGE_OPEN, + SAILING_BOAT_CARGO_HOLD_OAK_LARGE, + SAILING_BOAT_CARGO_HOLD_OAK_LARGE_OPEN, + SAILING_BOAT_CARGO_HOLD_TEAK_LARGE, + SAILING_BOAT_CARGO_HOLD_TEAK_LARGE_OPEN, + SAILING_BOAT_CARGO_HOLD_MAHOGANY_LARGE, + SAILING_BOAT_CARGO_HOLD_MAHOGANY_LARGE_OPEN, + SAILING_BOAT_CARGO_HOLD_CAMPHOR_LARGE, + SAILING_BOAT_CARGO_HOLD_CAMPHOR_LARGE_OPEN, + SAILING_BOAT_CARGO_HOLD_IRONWOOD_LARGE, + SAILING_BOAT_CARGO_HOLD_IRONWOOD_LARGE_OPEN, + SAILING_BOAT_CARGO_HOLD_ROSEWOOD_LARGE, + SAILING_BOAT_CARGO_HOLD_ROSEWOOD_LARGE_OPEN + }; + return Rs2GameObject.interact(SAILING_BOAT_CARGO_HOLDS, "open"); + } + + public Map getPortTasksVarbits() + { + return Arrays.stream(PortTaskVarbits.values()) + .map(x -> Map.entry(x, Microbot.getVarbitValue(x.getId()))) + .filter(e -> e.getValue() > 0 && e.getKey().getType() == PortTaskVarbits.TaskType.ID) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + public PortTaskData getPortTaskData(int varbitValue) + { + if (varbitValue <= 0) + { + return null; + } + + return Arrays.stream(PortTaskData.values()) + .filter(x -> x.getId() == varbitValue) + .findFirst() + .orElse(null); + } + + +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/NpcApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/NpcApiExample.java new file mode 100644 index 00000000000..fea7bdad501 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/NpcApiExample.java @@ -0,0 +1,71 @@ +package net.runelite.client.plugins.microbot.api.npc; + +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; + +import java.util.List; + +/** + * Example usage of the NPC API + * + * This demonstrates how to query NPCs using the new API structure: + * - Rs2NpcCache: Caches NPCs for efficient querying + * - Rs2NpcQueryable: Provides a fluent interface for filtering and querying NPCs + */ +public class NpcApiExample { + + public static void examples() { + // Create a new cache instance + Rs2NpcCache cache = new Rs2NpcCache(); + + // Example 1: Get the nearest NPC + Rs2NpcModel nearestNpc = cache.query().nearest(); + + // Example 2: Get the nearest NPC within 10 tiles + Rs2NpcModel nearestNpcWithinRange = cache.query().nearest(10); + + // Example 3: Find an NPC by name + Rs2NpcModel goblin = cache.query().withName("Goblin").nearest(); + + // Example 4: Find an NPC by multiple names + Rs2NpcModel enemy = cache.query().withNames("Goblin", "Guard", "Dark wizard").nearest(); + + // Example 5: Find an NPC by ID + Rs2NpcModel npcById = cache.query().withId(1234).nearest(); + + // Example 6: Find an NPC by multiple IDs + Rs2NpcModel npcByIds = cache.query().withIds(1234, 5678, 9012).nearest(); + + // Example 7: Get all NPCs with a custom filter + Rs2NpcModel attackingNpc = cache.query() + .where(npc -> npc.isInteractingWithPlayer()) + .first(); + + // Example 8: Chain multiple filters + Rs2NpcModel lowHealthEnemy = cache.query() + .where(npc -> npc.getName() != null && npc.getName().contains("Goblin")) + .where(npc -> npc.getHealthPercentage() < 50) + .nearest(); + + // Example 9: Get all NPCs matching criteria as a list + List allGoblins = cache.query() + .where(npc -> npc.getName() != null && npc.getName().equalsIgnoreCase("Goblin")) + .toList(); + + // Example 10: Complex query - Find nearest low health NPC within 15 tiles + Rs2NpcModel target = cache.query() + .where(npc -> npc.getHealthPercentage() > 0 && npc.getHealthPercentage() < 30) + .where(npc -> !npc.isDead()) + .nearest(15); + + // Example 11: Find NPCs that are moving + List movingNpcs = cache.query() + .where(Rs2NpcModel::isMoving) + .toList(); + + // Example 12: Static method to get stream directly + Rs2NpcModel firstNpc = Rs2NpcCache.getNpcsStream() + .filter(npc -> npc.getName() != null) + .findFirst() + .orElse(null); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCache.java new file mode 100644 index 00000000000..40e4c65b959 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCache.java @@ -0,0 +1,52 @@ +package net.runelite.client.plugins.microbot.api.npc; + +import net.runelite.api.WorldView; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Rs2NpcCache { + + private static int lastUpdateNpcs = 0; + private static List npcs = new ArrayList<>(); + + public Rs2NpcQueryable query() { + return new Rs2NpcQueryable(); + } + + /** + * Get all NPCs in the current scene across all world views + * + * @return Stream of Rs2NpcModel + */ + public static Stream getNpcsStream() { + + if (lastUpdateNpcs >= Microbot.getClient().getTickCount()) { + return npcs.stream(); + } + + List result = new ArrayList<>(); + + for (var id : Microbot.getWorldViewIds()) { + WorldView worldView = Microbot.getClient().getWorldView(id); + if (worldView == null) { + continue; + } + + result.addAll(worldView.npcs() + .stream() + .filter(Objects::nonNull) + .map(Rs2NpcModel::new) + .collect(Collectors.toList())); + } + + npcs = result; + lastUpdateNpcs = Microbot.getClient().getTickCount(); + return result.stream(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcQueryable.java new file mode 100644 index 00000000000..052fddee7d1 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcQueryable.java @@ -0,0 +1,20 @@ +package net.runelite.client.plugins.microbot.api.npc; + +import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; + +import java.util.stream.Stream; + +public final class Rs2NpcQueryable extends AbstractEntityQueryable + implements IEntityQueryable { + + public Rs2NpcQueryable() { + super(); + } + + @Override + protected Stream initialSource() { + return Rs2NpcCache.getNpcsStream(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModel.java new file mode 100644 index 00000000000..d3e392f0035 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModel.java @@ -0,0 +1,296 @@ +package net.runelite.client.plugins.microbot.api.npc.models; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.IEntity; +import net.runelite.client.plugins.microbot.api.actor.Rs2ActorModel; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; +import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; +import net.runelite.client.plugins.microbot.util.misc.Rs2UiHelper; +import net.runelite.client.plugins.microbot.util.tile.Rs2Tile; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; + +import java.util.Arrays; +import java.util.function.Predicate; +import java.util.stream.IntStream; + +@Getter +@Slf4j +public class Rs2NpcModel extends Rs2ActorModel implements IEntity +{ + + private final NPC npc; + + public Rs2NpcModel(final NPC npc) + { + super(npc); + this.npc = npc; + } + + @Override + public int getId() + { + return npc.getId(); + } + + + // Enhanced utility methods for cache operations + + /** + * Checks if this NPC is within a specified distance from the player. + * Uses client thread for safe access to player location. + * + * @param maxDistance Maximum distance in tiles + * @return true if within distance, false otherwise + */ + public boolean isWithinDistanceFromPlayer(int maxDistance) { + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + return this.getLocalLocation().distanceTo( + Microbot.getClient().getLocalPlayer().getLocalLocation()) <= maxDistance; + }).orElse(false); + } + + /** + * Gets the distance from this NPC to the player. + * Uses client thread for safe access to player location. + * + * @return Distance in tiles + */ + public int getDistanceFromPlayer() { + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + return this.getLocalLocation().distanceTo( + Microbot.getClient().getLocalPlayer().getLocalLocation()); + }).orElse(Integer.MAX_VALUE); + } + + /** + * Checks if this NPC is within a specified distance from a given location. + * + * @param anchor The anchor point + * @param maxDistance Maximum distance in tiles + * @return true if within distance, false otherwise + */ + public boolean isWithinDistance(WorldPoint anchor, int maxDistance) { + if (anchor == null) return false; + return getWorldLocation().distanceTo(anchor) <= maxDistance; + } + + /** + * Checks if this NPC is currently interacting with the player. + * Uses client thread for safe access to player reference. + * + * @return true if interacting with player, false otherwise + */ + public boolean isInteractingWithPlayer() { + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + return this.getInteracting() == Microbot.getClient().getLocalPlayer(); + }).orElse(false); + } + + /** + * Checks if this NPC is currently moving. + * + * @return true if moving, false if idle + */ + public boolean isMoving() { + + return Microbot.getClientThread().runOnClientThreadOptional(() -> + this.getPoseAnimation() != this.getIdlePoseAnimation() + ).orElse(false); + } + + /** + * Gets the health percentage of this NPC. + * + * @return Health percentage (0-100), or -1 if unknown + */ + public double getHealthPercentage() { + int ratio = this.getHealthRatio(); + int scale = this.getHealthScale(); + + if (scale == 0) return -1; + return (double) ratio / (double) scale * 100.0; + } + + public static Predicate matches(boolean exact, String... names) { + return npc -> { + String npcName = npc.getName(); + if (npcName == null) return false; + if (exact) npcName = npcName.toLowerCase(); + final String name = npcName; + return exact ? Arrays.stream(names).anyMatch(name::equalsIgnoreCase) : + Arrays.stream(names).anyMatch(s -> name.contains(s.toLowerCase())); + }; + } + + /** + * Gets the overhead prayer icon of the NPC, if any. + * @return + */ + public HeadIcon getHeadIcon() { + if (npc == null) { + return null; + } + + if (npc.getOverheadSpriteIds() == null) { + Microbot.log("Failed to find the correct overhead prayer."); + return null; + } + + for (int i = 0; i < npc.getOverheadSpriteIds().length; i++) { + int overheadSpriteId = npc.getOverheadSpriteIds()[i]; + + if (overheadSpriteId == -1) continue; + + return HeadIcon.values()[overheadSpriteId]; + } + + Microbot.log("Found overheadSpriteIds: " + Arrays.toString(npc.getOverheadSpriteIds()) + " but failed to find valid overhead prayer."); + + return null; + } + + public boolean hasLineOfSight() { + if (npc == null) return false; + + final WorldPoint npcLoc = getWorldLocation(); + if (npcLoc == null) return false; + + final WorldPoint myLoc = new Rs2PlayerModel().getWorldLocation(); + if (myLoc == null) return false; + + if (npcLoc.equals(myLoc)) return true; + + final WorldView wv = Microbot.getClient().getTopLevelWorldView(); + return wv != null && npcLoc.toWorldArea().hasLineOfSightTo(wv, myLoc); + } + + @Override + public boolean click() { + return click(""); + } + + @Override + public boolean click(String action) { + if (npc == null) { + log.error("Error interacting with NPC for action '{}': NPC is null", action); + return false; + } + + Microbot.status = action + " " + npc.getName(); + try { + if (Microbot.isCantReachTargetDetectionEnabled && Microbot.cantReachTarget) { + if (!hasLineOfSight()) { + if (Microbot.cantReachTargetRetries >= Rs2Random.between(3, 5)) { + Microbot.pauseAllScripts.compareAndSet(false, true); + Microbot.showMessage("Your bot tried to interact with an NPC for " + + Microbot.cantReachTargetRetries + " times but failed. Please take a look at what is happening."); + return false; + } + final WorldPoint npcWorldPoint = getWorldLocation(); + if (npcWorldPoint == null) { + log.error("Error interacting with NPC '{}' for action '{}': WorldPoint is null", getName(), action); + return false; + } + Rs2Walker.walkTo(Rs2Tile.getNearestWalkableTileWithLineOfSight(npcWorldPoint), 0); + Microbot.pauseAllScripts.compareAndSet(true, false); + Microbot.cantReachTargetRetries++; + return false; + } else { + Microbot.pauseAllScripts.compareAndSet(true, false); + Microbot.cantReachTarget = false; + Microbot.cantReachTargetRetries = 0; + } + } + + final NPCComposition npcComposition = Microbot.getClientThread().runOnClientThreadOptional( + () -> Microbot.getClient().getNpcDefinition(getId())).orElse(null); + if (npcComposition == null) { + log.error("Error interacting with NPC '{}' for action '{}': NPCComposition is null", getName(), action); + return false; + } + + final String[] actions = npcComposition.getActions(); + if (actions == null) { + log.error("Error interacting with NPC '{}' for action '{}': Actions are null", npc.getName(), action); + return false; + } + + final int index; + if (action == null || action.isBlank()) { + index = IntStream.range(0, actions.length) + .filter(i -> actions[i] != null && !actions[i].isEmpty()) + .findFirst().orElse(-1); + } else { + final String finalAction = action; + index = IntStream.range(0, actions.length) + .filter(i -> actions[i] != null && actions[i].equalsIgnoreCase(finalAction)) + .findFirst().orElse(-1); + } + + final MenuAction menuAction = getMenuAction(index); + if (menuAction == null) { + if (index == -1) { + log.error("Error interacting with NPC '{}' for action '{}': Action not found. Actions={}", npc.getName(), action, actions); + } else { + log.error("Error interacting with NPC '{}' for action '{}': Invalid Index={}. Actions={}", npc.getName(), action, index, actions); + } + return false; + } + action = menuAction == MenuAction.WIDGET_TARGET_ON_NPC ? "Use" : actions[index]; + + final LocalPoint localPoint = npc.getLocalLocation(); + if (localPoint == null) { + log.error("Error interacting with NPC '{}' for action '{}': LocalPoint is null", npc.getName(), action); + return false; + } + if (!Rs2Camera.isTileOnScreen(localPoint)) { + Rs2Camera.turnTo(npc); + } + + Microbot.doInvoke(new NewMenuEntry() + .param0(0) + .param1(0) + .opcode(menuAction.getId()) + .identifier(npc.getIndex()) + .itemId(-1) + .target(npc.getName()) + .actor(npc) + .option(action) + , + Rs2UiHelper.getActorClickbox(npc)); + return true; + + } catch (Exception ex) { + log.error("Error interacting with NPC '{}' for action '{}': ", npc.getName(), action, ex); + return false; + } + } + + private MenuAction getMenuAction(int index) { + if (Microbot.getClient().isWidgetSelected()) { + return MenuAction.WIDGET_TARGET_ON_NPC; + } + + switch (index) { + case 0: + return MenuAction.NPC_FIRST_OPTION; + case 1: + return MenuAction.NPC_SECOND_OPTION; + case 2: + return MenuAction.NPC_THIRD_OPTION; + case 3: + return MenuAction.NPC_FOURTH_OPTION; + case 4: + return MenuAction.NPC_FIFTH_OPTION; + default: + return null; + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCache.java new file mode 100644 index 00000000000..11725748625 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCache.java @@ -0,0 +1,50 @@ +package net.runelite.client.plugins.microbot.api.player; + +import net.runelite.api.WorldView; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Rs2PlayerCache { + + private static int lastUpdatePlayers = 0; + private static List players = new ArrayList<>(); + + public Rs2PlayerQueryable query() { + return new Rs2PlayerQueryable(); + } + + /** + * Get all players in the current scene + * @return Stream of Rs2PlayerModel + */ + public static Stream getPlayersStream() { + + if (lastUpdatePlayers >= Microbot.getClient().getTickCount()) { + return players.stream(); + } + + List result = new ArrayList<>(); + + for (var id : Microbot.getWorldViewIds()) { + WorldView worldView = Microbot.getClient().getWorldView(id); + if (worldView == null) { + continue; + } + result.addAll(worldView.players() + .stream() + .filter(Objects::nonNull) + .map(Rs2PlayerModel::new) + .collect(Collectors.toList())); + } + + players = result; + lastUpdatePlayers = Microbot.getClient().getTickCount(); + return players.stream(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerQueryable.java new file mode 100644 index 00000000000..78f8f7a16c8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerQueryable.java @@ -0,0 +1,20 @@ +package net.runelite.client.plugins.microbot.api.player; + +import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; + +import java.util.stream.Stream; + +public final class Rs2PlayerQueryable extends AbstractEntityQueryable + implements IEntityQueryable { + + public Rs2PlayerQueryable() { + super(); + } + + @Override + protected Stream initialSource() { + return Rs2PlayerCache.getPlayersStream(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/data/SalvagingAnimations.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/data/SalvagingAnimations.java new file mode 100644 index 00000000000..49820dbafa7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/data/SalvagingAnimations.java @@ -0,0 +1,80 @@ +package net.runelite.client.plugins.microbot.api.player.data; + +import static net.runelite.api.gameval.AnimationID.*; +import static net.runelite.api.gameval.AnimationID.HUMAN_SAILING_SALVAGE01_LARGE01_DROP01; +import static net.runelite.api.gameval.AnimationID.HUMAN_SAILING_SALVAGE01_LARGE01_IDLE01; +import static net.runelite.api.gameval.AnimationID.HUMAN_SAILING_SALVAGE01_LARGE01_INTERACT01; +import static net.runelite.api.gameval.AnimationID.HUMAN_SAILING_SALVAGE01_LARGE01_RESET01; +import static net.runelite.api.gameval.AnimationID.SAILING_BOATS_SALVAGE_HOOK_KANDARIN_1X3_RESET01; +import static net.runelite.api.gameval.AnimationID.SAILING_BOATS_SALVAGE_HOOK_KANDARIN_1X3_SALVAGING01; +import static net.runelite.api.gameval.AnimationID.SAILING_BOATS_SALVAGE_HOOK_KANDARIN_2X5_DROP01; +import static net.runelite.api.gameval.AnimationID.SAILING_BOATS_SALVAGE_HOOK_KANDARIN_2X5_IDLE01; +import static net.runelite.api.gameval.AnimationID.SAILING_BOATS_SALVAGE_HOOK_KANDARIN_2X5_IDLE02; +import static net.runelite.api.gameval.AnimationID.SAILING_BOATS_SALVAGE_HOOK_KANDARIN_2X5_RESET01; +import static net.runelite.api.gameval.AnimationID.SAILING_BOATS_SALVAGE_HOOK_KANDARIN_3X8_DROP01; +import static net.runelite.api.gameval.AnimationID.SAILING_BOATS_SALVAGE_HOOK_KANDARIN_3X8_IDLE01; +import static net.runelite.api.gameval.AnimationID.SAILING_BOATS_SALVAGE_HOOK_KANDARIN_3X8_IDLE02; +import static net.runelite.api.gameval.AnimationID.SAILING_BOATS_SALVAGE_HOOK_KANDARIN_3X8_RESET01; +import static net.runelite.api.gameval.AnimationID.SAILING_BOATS_SALVAGE_HOOK_KANDARIN_UI; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_DROP01; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_IDLE01; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_INACTIVE01; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_INTERACT01; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_PULL01; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_RESET01; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_SALVAGING01; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_2X5_DROP01; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_2X5_IDLE01; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_2X5_RESET01; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_3X8_DROP01; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_3X8_IDLE01; +import static net.runelite.api.gameval.AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_3X8_RESET01; +import static net.runelite.api.gameval.AnimationID.SAILING_SALVAGE01_LARGE01_DROP01; +import static net.runelite.api.gameval.AnimationID.SAILING_SALVAGE01_LARGE01_IDLE01; +import static net.runelite.api.gameval.AnimationID.SAILING_SALVAGE01_LARGE01_IDLE02; +import static net.runelite.api.gameval.AnimationID.SAILING_SALVAGE01_LARGE01_RESET01; + +public class SalvagingAnimations { + public static final int[] SALVAGING_ANIMATIONS = { + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_1X3_IDLE01, + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_1X3_DROP01, + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_1X3_IDLE02, + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_1X3_RESET01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_DROP01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_IDLE01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_RESET01, + + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_2X5_IDLE01, + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_2X5_DROP01, + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_2X5_IDLE02, + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_2X5_RESET01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_2X5_DROP01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_2X5_IDLE01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_2X5_RESET01, + + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_3X8_IDLE01, + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_3X8_DROP01, + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_3X8_IDLE02, + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_3X8_RESET01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_3X8_DROP01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_3X8_IDLE01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_3X8_RESET01, + + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_UI, + + SAILING_SALVAGE01_LARGE01_IDLE01, + SAILING_SALVAGE01_LARGE01_DROP01, + SAILING_SALVAGE01_LARGE01_IDLE02, + SAILING_SALVAGE01_LARGE01_RESET01, + HUMAN_SAILING_SALVAGE01_LARGE01_DROP01, + HUMAN_SAILING_SALVAGE01_LARGE01_IDLE01, + HUMAN_SAILING_SALVAGE01_LARGE01_INTERACT01, + HUMAN_SAILING_SALVAGE01_LARGE01_RESET01, + + SAILING_BOATS_SALVAGE_HOOK_KANDARIN_1X3_SALVAGING01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_SALVAGING01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_INACTIVE01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_PULL01, + SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_INTERACT01 + }; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/models/Rs2PlayerModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/models/Rs2PlayerModel.java new file mode 100644 index 00000000000..e350b9f016d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/models/Rs2PlayerModel.java @@ -0,0 +1,84 @@ +package net.runelite.client.plugins.microbot.api.player.models; + +import java.awt.Polygon; +import lombok.Getter; +import net.runelite.api.HeadIcon; +import net.runelite.api.Player; +import net.runelite.api.PlayerComposition; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.IEntity; +import net.runelite.client.plugins.microbot.api.actor.Rs2ActorModel; +import net.runelite.client.plugins.microbot.api.boat.Rs2BoatCache; +import net.runelite.client.plugins.microbot.api.player.data.SalvagingAnimations; +import net.runelite.client.plugins.microbot.util.ActorModel; + +@Getter +public class Rs2PlayerModel extends Rs2ActorModel implements IEntity { + + private final Player player; + + public Rs2PlayerModel() + { + super(Microbot.getClient().getLocalPlayer()); + this.player = Microbot.getClient().getLocalPlayer(); + } + + public Rs2PlayerModel(final Player player) + { + super(player); + this.player = player; + } + + @Override + public WorldPoint getWorldLocation() + { + return super.getWorldLocation(); + } + + @Override + public int getId() + { + return player.getId(); + } + + + /** + * Player interactions are not currently implemented. + * Player-to-player interactions in RuneScape are complex and context-dependent + * (PvP, trading, following, etc.) and require careful handling to avoid misuse. + * + * @return false (not implemented) + */ + @Override + public boolean click() { + return false; + } + + /** + * Player interactions are not currently implemented. + * Player-to-player interactions in RuneScape are complex and context-dependent + * (PvP, trading, following, etc.) and require careful handling to avoid misuse. + * + * For PvP interactions, consider using Rs2Combat utilities directly. + * For trading, use Rs2Trade utilities (if available). + * + * @param action the intended action (not implemented) + * @return false (not implemented) + */ + @Override + public boolean click(String action) { + return false; + } + + // Sailing stuff + public boolean isSalvaging() { + int anim = new Rs2PlayerModel().getAnimation(); + for (int id : SalvagingAnimations.SALVAGING_ANIMATIONS) { + if (anim == id) { + return true; + } + } + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/playerstate/Rs2PlayerStateCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/playerstate/Rs2PlayerStateCache.java new file mode 100644 index 00000000000..ceaeafcaca2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/playerstate/Rs2PlayerStateCache.java @@ -0,0 +1,177 @@ +package net.runelite.client.plugins.microbot.api.playerstate; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.Quest; +import net.runelite.api.QuestState; +import net.runelite.api.annotations.Varbit; +import net.runelite.api.annotations.Varp; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.VarbitChanged; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.questhelper.questinfo.QuestHelperQuest; + +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Caches player state data such as quest states, varbits, and varps. + * This cache is event-driven and automatically updates when game state changes. + */ +@Singleton +@Slf4j +public final class Rs2PlayerStateCache { + @Inject + private Client client; + @Inject + private ClientThread clientThread; + @Inject + private EventBus eventBus; + + @Getter + private final ConcurrentHashMap quests = new ConcurrentHashMap<>(); + private final ConcurrentHashMap varbits = new ConcurrentHashMap<>(); + private final ConcurrentHashMap varps = new ConcurrentHashMap<>(); + + volatile boolean questsPopulated = false; + + @Inject + public Rs2PlayerStateCache(EventBus eventBus, Client client, ClientThread clientThread) { + this.eventBus = eventBus; + this.client = client; + this.clientThread = clientThread; + + eventBus.register(this); + } + + + @Subscribe + private void onGameStateChanged(GameStateChanged e) { + if (e.getGameState() == GameState.LOGGED_IN) { + populateQuests(); + } + if (e.getGameState() == GameState.LOGIN_SCREEN) { + questsPopulated = false; + quests.clear(); + varbits.clear(); + varps.clear(); + } + } + + @Subscribe + public void onVarbitChanged(VarbitChanged event) { + if (questsPopulated) { + updateQuest(event); + } + if (event.getVarbitId() != -1) { + varbits.put(event.getVarbitId(), event.getValue()); + } + if (event.getVarpId() != -1) { + varps.put(event.getVarpId(), event.getValue()); + } + } + + /** + * Update the quest state for a specific quest based on a varbit change event. + * + * @param event + */ + private void updateQuest(VarbitChanged event) { + QuestHelperQuest quest = Arrays.stream(QuestHelperQuest.values()) + .filter(x -> x.getVarbit() != null) + .filter(x -> x.getVarbit().getId() == event.getVarbitId()) + .findFirst() + .orElse(null); + + if (quest != null) { + QuestState questState = quest.getState(client); + quests.put(quest.getId(), questState); + } + } + + /** + * Populate the quests map with the current quest states. + */ + private void populateQuests() { + clientThread.invokeLater(() -> + { + for (Quest quest : Quest.values()) { + QuestState questState = quest.getState(client); + quests.put(quest.getId(), questState); + } + questsPopulated = true; + }); + } + + /** + * Get the quest state for a specific quest. + * + * @param quest + * @return + */ + public QuestState getQuestState(Quest quest) { + return quests.get(quest.getId()); + } + + /** + * Get the value of a specific varbit. + * + * @param varbitId + * @return + */ + public @Varbit int getVarbitValue(@Varbit int varbitId) { + Integer cached = varbits.get(varbitId); + + if (cached != null) { + return cached; + } + + int value = updateVarbitValue(varbitId); + + return value; + } + + private @Varbit int updateVarbitValue(@Varbit int varbitId) { + int value; + value = Microbot.getClientThread().runOnClientThreadOptional(() -> client.getVarbitValue(varbitId)).orElse(0); + + varbits.put(varbitId, value); + return value; + } + + /** + * Get the value of a specific varp. + * + * @param varbitId + * @return + */ + public @Varp int getVarpValue(@Varp int varbitId) { + Integer cached = varps.get(varbitId); + + if (cached != null) { + return cached; + } + + int value = updateVarpValue(varbitId); + + return value; + } + + private @Varp int updateVarpValue(@Varp int varpId) { + int value; + + value = Microbot.getClientThread().runOnClientThreadOptional(() -> client.getVarpValue(varpId)).orElse(0); + + if (value > 0) { + varps.put(varpId, value); + } + return value; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemCache.java new file mode 100644 index 00000000000..b7e9295f15f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemCache.java @@ -0,0 +1,72 @@ +package net.runelite.client.plugins.microbot.api.tileitem; + +import net.runelite.api.Player; +import net.runelite.api.Tile; +import net.runelite.api.TileItem; +import net.runelite.api.WorldView; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +/** + * Cache for tile items in the current scene. + * Uses polling-based approach to ensure reliability, as ItemSpawned/ItemDespawned events + * are not always triggered consistently. + */ +public class Rs2TileItemCache { + + private static int lastUpdateTick = 0; + private static List tileItems = new ArrayList<>(); + + public Rs2TileItemQueryable query() { + return new Rs2TileItemQueryable(); + } + + /** + * Get all tile items in the current scene across all world views. + * Refreshes the cache once per game tick by polling all tiles. + * This ensures reliability even when ItemSpawned/ItemDespawned events don't fire. + * + * @return Stream of Rs2TileItemModel + */ + public static Stream getTileItemsStream() { + if (lastUpdateTick >= Microbot.getClient().getTickCount()) { + return tileItems.stream(); + } + + Player player = Microbot.getClient().getLocalPlayer(); + if (player == null) return Stream.empty(); + + List result = new ArrayList<>(); + + for (var id : Microbot.getWorldViewIds()) { + WorldView worldView = Microbot.getClient().getWorldView(id); + if (worldView == null) { + continue; + } + + Tile[][] tiles = worldView.getScene().getTiles()[worldView.getPlane()]; + for (Tile[] tileRow : tiles) { + for (Tile tile : tileRow) { + if (tile == null) continue; + + List items = tile.getGroundItems(); + if (items == null || items.isEmpty()) continue; + + for (TileItem item : items) { + if (item != null) { + result.add(new Rs2TileItemModel(tile, item)); + } + } + } + } + } + + tileItems = result; + lastUpdateTick = Microbot.getClient().getTickCount(); + return result.stream(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemQueryable.java new file mode 100644 index 00000000000..bb549b26def --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemQueryable.java @@ -0,0 +1,20 @@ +package net.runelite.client.plugins.microbot.api.tileitem; + +import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; + +import java.util.stream.Stream; + +public final class Rs2TileItemQueryable extends AbstractEntityQueryable + implements IEntityQueryable { + + public Rs2TileItemQueryable() { + super(); + } + + @Override + protected Stream initialSource() { + return Rs2TileItemCache.getTileItemsStream(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/TileItemApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/TileItemApiExample.java new file mode 100644 index 00000000000..1b5adfdb174 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/TileItemApiExample.java @@ -0,0 +1,104 @@ +package net.runelite.client.plugins.microbot.api.tileitem; + +import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; + +import javax.inject.Inject; +import java.util.List; + +/** + * Example usage of the Ground Item API + * This demonstrates how to query ground items using the new API structure: + * - Rs2GroundItemCache: Caches ground items for efficient querying + * - Rs2GroundItemQueryable: Provides a fluent interface for filtering and querying ground items + */ +public class TileItemApiExample { + + @Inject + Rs2TileItemCache cache; + + public void examples() { + // Create a new cache instance + + // Example 1: Get the nearest ground item + Rs2TileItemModel nearestItem = cache.query().nearest(); + + // Example 2: Get the nearest ground item within 10 tiles + Rs2TileItemModel nearestItemWithinRange = cache.query().nearest(10); + + // Example 3: Find a ground item by name + Rs2TileItemModel coins = cache.query().withName("Coins").nearest(); + + // Example 4: Find a ground item by multiple names + Rs2TileItemModel loot = cache.query().withNames("Dragon bones", "Dragon scale", "Dragon dagger").nearest(); + + // Example 5: Find a ground item by ID + Rs2TileItemModel itemById = cache.query().withId(995).nearest(); // Coins + + // Example 6: Find a ground item by multiple IDs + Rs2TileItemModel itemByIds = cache.query().withIds(995, 526, 537).nearest(); // Coins, Bones, Dragon bones + + // Example 7: Get all ground items worth more than 1000 gp + List valuableItems = cache.query() + .where(item -> item.getTotalValue() >= 1000) + .toList(); + + // Example 8: Find nearest lootable item + Rs2TileItemModel lootableItem = cache.query() + .where(Rs2TileItemModel::isLootAble) + .nearest(); + + // Example 9: Find items owned by player + List ownedItems = cache.query() + .where(Rs2TileItemModel::isOwned) + .toList(); + + // Example 10: Find stackable items + List stackableItems = cache.query() + .where(Rs2TileItemModel::isStackable) + .toList(); + + // Example 11: Find noted items + List notedItems = cache.query() + .where(Rs2TileItemModel::isNoted) + .toList(); + + // Example 13: Find items about to despawn + List despawningItems = cache.query() + .where(item -> item.willDespawnWithin(30)) + .toList(); + + // Example 16: Complex query - Find nearest valuable lootable item within 15 tiles + Rs2TileItemModel target = cache.query() + .where(Rs2TileItemModel::isLootAble) + .where(item -> item.getTotalGeValue() >= 5000) + .where(item -> item.isDespawned()) + .nearest(15); + + // Example 17: Find items by quantity + List largeStacks = cache.query() + .where(item -> item.getQuantity() >= 100) + .toList(); + + // Example 18: Find tradeable items + List tradeableItems = cache.query() + .where(Rs2TileItemModel::isTradeable) + .toList(); + + // Example 19: Find members items + List membersItems = cache.query() + .where(Rs2TileItemModel::isMembers) + .toList(); + + // Example 20: Static method to get stream directly + Rs2TileItemModel firstItem = Rs2TileItemCache.getTileItemsStream() + .filter(item -> item.getName() != null) + .findFirst() + .orElse(null); + + // Example 21: Find items by partial name match + List herbItems = cache.query() + .where(item -> item.getName() != null && + item.getName().toLowerCase().contains("herb")) + .toList(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/models/Rs2TileItemModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/models/Rs2TileItemModel.java new file mode 100644 index 00000000000..c691c6a222e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/models/Rs2TileItemModel.java @@ -0,0 +1,269 @@ +package net.runelite.client.plugins.microbot.api.tileitem.models; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.IEntity; +import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.reflection.Rs2Reflection; + +import java.awt.*; +import java.util.function.Supplier; + +@Slf4j +public class Rs2TileItemModel implements TileItem, IEntity { + + @Getter + private final Tile tile; + @Getter + private final TileItem tileItem; + + public Rs2TileItemModel(Tile tileObject, TileItem tileItem) { + this.tile = tileObject; + this.tileItem = tileItem; + } + + @Override + public int getId() { + return tileItem.getId(); + } + + @Override + public int getQuantity() { + return tileItem.getQuantity(); + } + + @Override + public int getVisibleTime() { + return tileItem.getVisibleTime(); + } + + @Override + public int getDespawnTime() { + return tileItem.getDespawnTime(); + } + + @Override + public int getOwnership() { + return tileItem.getOwnership(); + } + + @Override + public boolean isPrivate() { + return tileItem.isPrivate(); + } + + @Override + public Model getModel() { + return tileItem.getModel(); + } + + @Override + public int getModelHeight() { + return tileItem.getModelHeight(); + } + + @Override + public void setModelHeight(int modelHeight) { + tileItem.setModelHeight(modelHeight); + } + + @Override + public int getAnimationHeightOffset() { + return tileItem.getAnimationHeightOffset(); + } + + @Override + public Node getNext() { + return tileItem.getNext(); + } + + @Override + public Node getPrevious() { + return tileItem.getPrevious(); + } + + @Override + public long getHash() { + return tileItem.getHash(); + } + + public String getName() { + return Microbot.getClientThread().invoke(() -> { + ItemComposition itemComposition = Microbot.getClient().getItemDefinition(tileItem.getId()); + return itemComposition.getName(); + }); + } + + public WorldPoint getWorldLocation() { + return tile.getWorldLocation(); + } + + public LocalPoint getLocalLocation() { + return tile.getLocalLocation(); + } + + @Override + public WorldView getWorldView() { + return Microbot.getClient().getTopLevelWorldView(); + } + + public boolean isNoted() { + return Microbot.getClientThread().invoke((Supplier ) () -> { + ItemComposition itemComposition = Microbot.getClient().getItemDefinition(tileItem.getId()); + return itemComposition.getNote() == 799; + }); + } + + + public boolean isStackable() { + return Microbot.getClientThread().invoke((Supplier ) () -> { + ItemComposition itemComposition = Microbot.getClient().getItemDefinition(tileItem.getId()); + return itemComposition.isStackable(); + }); + } + + public boolean isProfitableToHighAlch() { + return Microbot.getClientThread().invoke((Supplier ) () -> { + ItemComposition itemComposition = Microbot.getClient().getItemDefinition(tileItem.getId()); + int highAlchValue = itemComposition.getPrice() * 60 / 100; + int marketPrice = Microbot.getItemManager().getItemPrice(itemComposition.getId()); + return marketPrice > highAlchValue; + }); + } + + public boolean willDespawnWithin(int ticks) { + return getDespawnTime() - Microbot.getClient().getTickCount() <= ticks; + } + + public boolean isLootAble() { + return !(tileItem.getOwnership() == TileItem.OWNERSHIP_OTHER); + } + + public boolean isOwned() { + return tileItem.getOwnership() == TileItem.OWNERSHIP_SELF; + } + + public boolean isDespawned() { + return getDespawnTime() > Microbot.getClient().getTickCount(); + } + + public int getTotalGeValue() { + return Microbot.getClientThread().invoke(() -> { + ItemComposition itemComposition = Microbot.getClient().getItemDefinition(tileItem.getId()); + int price = itemComposition.getPrice(); + return price * tileItem.getQuantity(); + }); + } + + public boolean isTradeable() { + return Microbot.getClientThread().invoke((Supplier ) () -> { + ItemComposition itemComposition = Microbot.getClient().getItemDefinition(tileItem.getId()); + return itemComposition.isTradeable(); + }); + } + + public boolean isMembers() { + return Microbot.getClientThread().invoke((Supplier ) () -> { + ItemComposition itemComposition = Microbot.getClient().getItemDefinition(tileItem.getId()); + return itemComposition.isMembers(); + }); + } + + public int getTotalValue() { + return Microbot.getClientThread().invoke(() -> { + ItemComposition itemComposition = Microbot.getClient().getItemDefinition(tileItem.getId()); + int price = Microbot.getItemManager().getItemPrice(itemComposition.getId()); + return price * tileItem.getQuantity(); + }); + } + + public boolean click() { + return click(""); + } + + public boolean click(String action) { + try { + int param0; + int param1; + int identifier; + String target; + MenuAction menuAction = MenuAction.CANCEL; + ItemComposition item; + + item = Microbot.getClientThread().runOnClientThreadOptional(() -> Microbot.getClient().getItemDefinition(getId())).orElse(null); + if (item == null) return false; + identifier = getId(); + + LocalPoint localPoint = getLocalLocation(); + if (localPoint == null) return false; + + param0 = localPoint.getSceneX(); + target = " " + getName(); + param1 = localPoint.getSceneY(); + + String[] groundActions = Rs2Reflection.getGroundItemActions(item); + + int index = -1; + if (action.isEmpty()) { + action = groundActions[0]; + } else { + for (int i = 0; i < groundActions.length; i++) { + String groundAction = groundActions[i]; + if (groundAction == null || !groundAction.equalsIgnoreCase(action)) continue; + index = i; + } + } + + if (Microbot.getClient().isWidgetSelected()) { + menuAction = MenuAction.WIDGET_TARGET_ON_GROUND_ITEM; + } else if (index == 0) { + menuAction = MenuAction.GROUND_ITEM_FIRST_OPTION; + } else if (index == 1) { + menuAction = MenuAction.GROUND_ITEM_SECOND_OPTION; + } else if (index == 2) { + menuAction = MenuAction.GROUND_ITEM_THIRD_OPTION; + } else if (index == 3) { + menuAction = MenuAction.GROUND_ITEM_FOURTH_OPTION; + } else if (index == 4) { + menuAction = MenuAction.GROUND_ITEM_FIFTH_OPTION; + } + LocalPoint localPoint1 = getLocalLocation(); + if (localPoint1 != null) { + Polygon canvas = Perspective.getCanvasTilePoly(Microbot.getClient(), localPoint1); + if (canvas != null) { + Microbot.doInvoke(new NewMenuEntry() + .option(action) + .param0(param0) + .param1(param1) + .opcode(menuAction.getId()) + .identifier(identifier) + .itemId(-1) + .target(target) + , + canvas.getBounds()); + } + } else { + Microbot.doInvoke(new NewMenuEntry() + .option(action) + .param0(param0) + .param1(param1) + .opcode(menuAction.getId()) + .identifier(identifier) + .itemId(-1) + .target(target) + , + new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + + } + } catch (Exception ex) { + Microbot.log(ex.getMessage()); + ex.printStackTrace(); + } + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectCache.java new file mode 100644 index 00000000000..ba96145d8d6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectCache.java @@ -0,0 +1,73 @@ +package net.runelite.client.plugins.microbot.api.tileobject; + +import net.runelite.api.GameObject; +import net.runelite.api.Player; +import net.runelite.api.Tile; +import net.runelite.api.WorldView; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +public class Rs2TileObjectCache { + + private static int lastUpdateObjects = 0; + private static List tileObjects = new ArrayList<>(); + + public Rs2TileObjectQueryable query() { + return new Rs2TileObjectQueryable(); + } + + /** + * Get all tile objects in the current scene + * + * @return Stream of Rs2TileObjectModel + */ + public static Stream getObjectsStream() { + + if (lastUpdateObjects >= Microbot.getClient().getTickCount()) { + return tileObjects.stream(); + } + + Player player = Microbot.getClient().getLocalPlayer(); + if (player == null) return Stream.empty(); + + List result = new ArrayList<>(); + + for (var id : Microbot.getWorldViewIds()) { + WorldView worldView = Microbot.getClient().getWorldView(id); + if (worldView == null) { + continue; + } + var tileValues = Microbot.getClient().getWorldView(worldView.getId()).getScene().getTiles()[worldView.getPlane()]; + for (Tile[] tileValue : tileValues) { + for (Tile tile : tileValue) { + if (tile == null) continue; + + if (tile.getGameObjects() != null) { + for (GameObject gameObject : tile.getGameObjects()) { + if (gameObject == null) continue; + if (gameObject.getSceneMinLocation().equals(tile.getSceneLocation())) { + result.add(new Rs2TileObjectModel(gameObject)); + } + } + } + if (tile.getGroundObject() != null) { + result.add(new Rs2TileObjectModel(tile.getGroundObject())); + } + if (tile.getWallObject() != null) { + result.add(new Rs2TileObjectModel(tile.getWallObject())); + } + if (tile.getDecorativeObject() != null) { + result.add(new Rs2TileObjectModel(tile.getDecorativeObject())); + } + } + } + } + tileObjects = result; + lastUpdateObjects = Microbot.getClient().getTickCount(); + return result.stream(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectQueryable.java new file mode 100644 index 00000000000..2d02e7347a7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectQueryable.java @@ -0,0 +1,20 @@ +package net.runelite.client.plugins.microbot.api.tileobject; + +import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; + +import java.util.stream.Stream; + +public final class Rs2TileObjectQueryable extends AbstractEntityQueryable + implements IEntityQueryable { + + public Rs2TileObjectQueryable() { + super(); + } + + @Override + protected Stream initialSource() { + return Rs2TileObjectCache.getObjectsStream(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModel.java new file mode 100644 index 00000000000..0c096203919 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModel.java @@ -0,0 +1,298 @@ +package net.runelite.client.plugins.microbot.api.tileobject.models; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import net.runelite.api.Point; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.IEntity; +import net.runelite.client.plugins.microbot.api.boat.Rs2BoatCache; +import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; +import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; +import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; +import net.runelite.client.plugins.microbot.util.misc.Rs2UiHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.Objects; + +import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; + +@Slf4j +public class Rs2TileObjectModel implements TileObject, IEntity { + + public Rs2TileObjectModel(GameObject gameObject) { + this.tileObject = gameObject; + this.tileObjectType = TileObjectType.GAME; + } + + public Rs2TileObjectModel(DecorativeObject tileObject) { + this.tileObject = tileObject; + this.tileObjectType = TileObjectType.DECORATIVE; + } + + public Rs2TileObjectModel(WallObject tileObject) { + this.tileObject = tileObject; + this.tileObjectType = TileObjectType.WALL; + } + + public Rs2TileObjectModel(GroundObject tileObject) { + this.tileObject = tileObject; + this.tileObjectType = TileObjectType.GROUND; + } + + public Rs2TileObjectModel(TileObject tileObject) { + this.tileObject = tileObject; + this.tileObjectType = TileObjectType.GENERIC; + } + + @Getter + private final TileObjectType tileObjectType; + private final TileObject tileObject; + private String[] actions; + + + @Override + public long getHash() { + return tileObject.getHash(); + } + + @Override + public int getX() { + return tileObject.getX(); + } + + @Override + public int getY() { + return tileObject.getY(); + } + + @Override + public int getZ() { + return tileObject.getZ(); + } + + @Override + public int getPlane() { + return tileObject.getPlane(); + } + + @Override + public WorldView getWorldView() { + return tileObject.getWorldView(); + } + + public int getId() { + return tileObject.getId(); + } + + @Override + public @NotNull WorldPoint getWorldLocation() { + WorldPoint worldLocation = tileObject.getWorldLocation(); + + if (!(tileObject instanceof GameObject)) { + return worldLocation; + } + + GameObject go = (GameObject) tileObject; + WorldView wv = getWorldView(); + Point sceneMin = go.getSceneMinLocation(); + + if (wv == null || sceneMin == null) { + return worldLocation; + } + + return WorldPoint.fromScene(wv, sceneMin.getX(), sceneMin.getY(), wv.getPlane()); + } + + public String getName() { + return Microbot.getClientThread().invoke(() -> { + ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); + if (composition.getImpostorIds() != null) { + composition = composition.getImpostor(); + } + if (composition == null) + return null; + return Rs2UiHelper.stripColTags(composition.getName()); + }); + } + + @Override + public @NotNull LocalPoint getLocalLocation() { + return tileObject.getLocalLocation(); + } + + @Override + public @Nullable Point getCanvasLocation() { + return tileObject.getCanvasLocation(); + } + + @Override + public @Nullable Point getCanvasLocation(int zOffset) { + return tileObject.getCanvasLocation(); + } + + @Override + public @Nullable Polygon getCanvasTilePoly() { + return tileObject.getCanvasTilePoly(); + } + + @Override + public @Nullable Point getCanvasTextLocation(Graphics2D graphics, String text, int zOffset) { + return tileObject.getCanvasTextLocation(graphics, text, zOffset); + } + + @Override + public @Nullable Point getMinimapLocation() { + return tileObject.getMinimapLocation(); + } + + @Override + public @Nullable Shape getClickbox() { + return tileObject.getClickbox(); + } + + @Override + public @Nullable String getOpOverride(int index) { + return tileObject.getOpOverride(index); + } + + @Override + public boolean isOpShown(int index) { + return tileObject.isOpShown(index); + } + + public ObjectComposition getObjectComposition() { + return Microbot.getClientThread().invoke(() -> { + ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); + if (composition.getImpostorIds() != null) { + composition = composition.getImpostor(); + } + return composition; + }); + } + + public boolean click() { + return click(""); + } + + /** + * Clicks on the specified tile object with no specific action. + * Delegates to Rs2GameObject.clickObject. + * + * @param action the action to perform (e.g., "Open", "Climb") + * @return true if the interaction was successful, false otherwise + */ + public boolean click(String action) { + try { + + int param0; + int param1; + MenuAction menuAction = MenuAction.WALK; + + + Microbot.status = action + " " + getName(); + + if (getTileObjectType() == TileObjectType.GAME) { + GameObject obj = (GameObject) tileObject; + if (obj.sizeX() > 1) { + param0 = obj.getLocalLocation().getSceneX() - obj.sizeX() / 2; + } else { + param0 = obj.getLocalLocation().getSceneX(); + } + + if (obj.sizeY() > 1) { + param1 = obj.getLocalLocation().getSceneY() - obj.sizeY() / 2; + } else { + param1 = obj.getLocalLocation().getSceneY(); + } + } else { + // Default objects like walls, groundobjects, decorationobjects etc... + param0 = getLocalLocation().getSceneX(); + param1 = getLocalLocation().getSceneY(); + } + + + int index = 0; + String objName = ""; + if (action != null) { + //performance improvement to only get compoisiton if action has been specified + var objComp = getObjectComposition(); + String[] actions; + if (objComp.getImpostorIds() != null && objComp.getImpostor() != null) { + actions = objComp.getImpostor().getActions(); + } else { + actions = objComp.getActions(); + } + + for (int i = 0; i < actions.length; i++) { + if (actions[i] == null) continue; + if (action.equalsIgnoreCase(Rs2UiHelper.stripColTags(actions[i]))) { + index = i; + break; + } + } + + if (index == actions.length) + index = 0; + + objName = objComp.getName(); + + // both hands must be free before using MINECART + if (objComp.getName().toLowerCase().contains("train cart")) { + Rs2Equipment.unEquip(EquipmentInventorySlot.WEAPON); + Rs2Equipment.unEquip(EquipmentInventorySlot.SHIELD); + sleepUntil(() -> Rs2Equipment.get(EquipmentInventorySlot.WEAPON) == null && Rs2Equipment.get(EquipmentInventorySlot.SHIELD) == null); + } + } + + if (index == -1) { + log.warn("Failed to interact with object {} - action '{}' not found", getId(), action); + } + + + if (Microbot.getClient().isWidgetSelected()) { + menuAction = MenuAction.WIDGET_TARGET_ON_GAME_OBJECT; + } else if (index == 0) { + menuAction = MenuAction.GAME_OBJECT_FIRST_OPTION; + } else if (index == 1) { + menuAction = MenuAction.GAME_OBJECT_SECOND_OPTION; + } else if (index == 2) { + menuAction = MenuAction.GAME_OBJECT_THIRD_OPTION; + } else if (index == 3) { + menuAction = MenuAction.GAME_OBJECT_FOURTH_OPTION; + } else if (index == 4) { + menuAction = MenuAction.GAME_OBJECT_FIFTH_OPTION; + } + + if (!Rs2Camera.isTileOnScreen(getLocalLocation())) { + Rs2Camera.turnTo(tileObject); + } + + + Microbot.doInvoke(new NewMenuEntry() + .param0(param0) + .param1(param1) + .opcode(menuAction.getId()) + .identifier(getId()) + .itemId(-1) + .option(action) + .target(objName) + .setWorldViewId(getWorldView().getId()) + .gameObject(tileObject) + , + Rs2UiHelper.getObjectClickbox(tileObject)); +// MenuEntryImpl(getOption=Use, getTarget=Barrier, getIdentifier=43700, getType=GAME_OBJECT_THIRD_OPTION, getParam0=53, getParam1=51, getItemId=-1, isForceLeftClick=true, getWorldViewId=-1, isDeprioritized=false) + //Rs2Reflection.invokeMenu(param0, param1, menuAction.getId(), object.getId(),-1, "", "", -1, -1); + + } catch (Exception ex) { + log.error("Failed to interact with object: ", ex); + } + + return true; + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/TileObjectType.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/TileObjectType.java new file mode 100644 index 00000000000..e2ac9c775d2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/TileObjectType.java @@ -0,0 +1,9 @@ +package net.runelite.client.plugins.microbot.api.tileobject.models; + +public enum TileObjectType { + GAME, + WALL, + DECORATIVE, + GROUND, + GENERIC +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerOverlay.java index 925d9967bc3..37066fab40a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerOverlay.java @@ -1,6 +1,5 @@ package net.runelite.client.plugins.microbot.breakhandler; -import net.runelite.client.plugins.microbot.pluginscheduler.util.SchedulerPluginUtil; import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; import net.runelite.client.ui.overlay.OverlayLayer; import net.runelite.client.ui.overlay.OverlayPanel; @@ -62,18 +61,10 @@ public Dimension render(Graphics2D graphics) { .rightColor(Color.RED) .build()); - // Show specific lock reason if it's manual lock vs plugin lock - if (BreakHandlerScript.lockState.get() && !SchedulerPluginUtil.hasLockedSchedulablePlugins()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Reason: Manual Lock") - .leftColor(Color.ORANGE) - .build()); - } else { - panelComponent.getChildren().add(LineComponent.builder() - .left("Reason: Plugin Lock Condition Active") - .leftColor(Color.ORANGE) - .build()); - } + panelComponent.getChildren().add(LineComponent.builder() + .left("Reason: Plugin Lock Condition Active") + .leftColor(Color.ORANGE) + .build()); } else { panelComponent.getChildren().add(LineComponent.builder() .left("Status: UNLOCKED") diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerScript.java index aee7ec76a54..bab66286ae3 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerScript.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/BreakHandlerScript.java @@ -3,7 +3,6 @@ import lombok.extern.slf4j.Slf4j; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; -import net.runelite.client.plugins.microbot.pluginscheduler.util.SchedulerPluginUtil; import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; import net.runelite.client.plugins.microbot.util.discord.Rs2Discord; import net.runelite.client.plugins.microbot.util.events.PluginPauseEvent; @@ -900,7 +899,7 @@ private void resetBreakState() { * This includes both the manual lock state and any locked conditions from schedulable plugins. */ public static boolean isLockState() { - return lockState.get() || SchedulerPluginUtil.hasLockedSchedulablePlugins(); + return lockState.get(); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScript.java index 8f3f573f190..f21dbca2388 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScript.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScript.java @@ -1,64 +1,149 @@ package net.runelite.client.plugins.microbot.example; import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Player; -import net.runelite.api.Scene; -import net.runelite.api.Tile; -import net.runelite.api.WorldView; +import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; import net.runelite.api.gameval.ObjectID; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; +import net.runelite.client.plugins.microbot.api.npc.Rs2NpcCache; +import net.runelite.client.plugins.microbot.api.tileitem.Rs2TileItemCache; +import net.runelite.client.plugins.microbot.api.tileobject.Rs2TileObjectCache; +import net.runelite.client.plugins.microbot.api.tileobject.models.TileObjectType; +import net.runelite.client.plugins.microbot.shortestpath.WorldPointUtil; +import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.sailing.data.BoatPathFollower; -import net.runelite.client.plugins.microbot.util.sailing.data.PortPaths; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import net.runelite.client.plugins.microbot.api.player.Rs2PlayerCache; +import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; +import net.runelite.client.plugins.microbot.util.reachable.Rs2Reachable; -import javax.inject.Singleton; +import javax.inject.Inject; +import java.util.ArrayList; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** * Performance test script for measuring GameObject composition retrieval speed. - * + * * This script runs every 5 seconds and performs the following: * - Gets all GameObjects in the scene * - Retrieves the ObjectComposition for each GameObject * - Measures and logs the total time taken * - Reports average time per object - * + *
* Useful for performance profiling and optimization testing. */ -@Singleton @Slf4j public class ExampleScript extends Script { + @Inject + Rs2TileItemCache rs2TileItemCache; + @Inject + Rs2TileObjectCache rs2TileObjectCache; + @Inject + Rs2PlayerCache rs2PlayerCache; + @Inject + Rs2NpcCache rs2NpcCache; /** * Main entry point for the performance test script. */ + private static final WorldPoint HOPPER_DEPOSIT_DOWN = new WorldPoint(3748, 5672, 0); + public boolean run() { - //var boatPathFollower = new BoatPathFollower(PortPaths.PORT_SARIM_PANDEMONIUM.getFullPath(true)); mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { if (!Microbot.isLoggedIn()) return; - System.out.println("hello world"); +/* + if (Microbot.getClient().getTopLevelWorldView().getScene().isInstance()) { + LocalPoint l = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), Microbot.getClient().getLocalPlayer().getWorldLocation()); + System.out.println("was here"); + WorldPoint.fromLocalInstance(Microbot.getClient(), l); + } else { + System.out.println("was here lol"); + // this needs to ran on client threaad if we are on the sea + var a = Microbot.getClient().getLocalPlayer().getWorldLocation(); + System.out.println(a); + }*/ + + var shipwreck = rs2TileObjectCache.query() + .where(x -> x.getName() != null && x.getName().toLowerCase().contains("shipwreck")) + .within(5) + .nearestOnClientThread(); + var player = new Rs2PlayerModel(); + + var isInvFull = Rs2Inventory.count() >= Rs2Random.between(24, 28); + if (isInvFull && Rs2Inventory.count("salvage") > 0 && player.getAnimation() == -1) { + // Rs2Inventory.dropAll("large salvage"); + rs2TileObjectCache.query() + .fromWorldView() + .where(x -> x.getName() != null && x.getName().equalsIgnoreCase("salvaging station")) + .where(x -> x.getWorldView().getId() == new Rs2PlayerModel().getWorldView().getId()) + .nearestOnClientThread() + .click(); + sleepUntil(() -> Rs2Inventory.count("salvage") == 0, 60000); + } else if (isInvFull) { + dropJunk(); + } else { + if (player.getAnimation() != -1) { + log.info("Currently salvaging, waiting..."); + sleep(5000, 10000); + return; + } - WorldPoint worldPoint = WorldPoint.fromRegion(Microbot.getClient().getLocalPlayer().getWorldLocation().getRegionID(), - 35, - 34, - Microbot.getClient().getTopLevelWorldView().getPlane()); + if (shipwreck == null) { + log.info("No shipwreck found nearby"); + sleep(5000); + dropJunk(); + return; + } - Rs2Bank.openBank(); + rs2TileObjectCache.query().fromWorldView().where(x -> x.getName() != null && x.getName().toLowerCase().contains("salvaging hook")).nearestOnClientThread().click("Deploy"); + sleepUntil(() -> player.getAnimation() != -1, 5000); - Rs2Bank.withdrawAll("coins"); + } } catch (Exception ex) { - log.error("Error test loop", ex); + log.error("Error in performance test loop", ex); } }, 0, 1000, TimeUnit.MILLISECONDS); return true; } + + private void dropJunk() { + var junkItems = new ArrayList
(); + junkItems.add("gold ring"); + junkItems.add("sapphire ring"); + junkItems.add("emerald ring"); + junkItems.add("ruby ring"); + junkItems.add("diamond ring"); + junkItems.add("casket"); + junkItems.add("oyster pearl"); + junkItems.add("oyster pearls"); + junkItems.add("teak logs"); + junkItems.add("steel nails"); + junkItems.add("mithril nails"); + junkItems.add("giant seaweed"); + junkItems.add("mithril cannonball"); + junkItems.add("adamant cannonball"); + junkItems.add("elkhorn frag"); + junkItems.add("plank"); + junkItems.add("oak plank"); + junkItems.add("hemp seed"); + junkItems.add("flax seed"); + junkItems.add("ruby bracelet"); + junkItems.add("emerald bracelet"); + junkItems.add("mithril scimitar"); + junkItems.add("mahogany repair kit"); + junkItems.add("teak repair kit"); + junkItems.add("rum"); + junkItems.add("diamond bracelet"); + junkItems.add("sapphire ring"); + junkItems.add("emerald ring"); + junkItems.add("emerald bracelet"); + Rs2Inventory.dropAll( junkItems.toArray(new String[0])); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScriptOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScriptOverlay.java index ca4df9e5b8c..55495887311 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScriptOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/example/ExampleScriptOverlay.java @@ -1,17 +1,30 @@ package net.runelite.client.plugins.microbot.example; -import net.runelite.api.Point; +import net.runelite.api.Client; +import net.runelite.api.Perspective; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.shortestpath.WorldPointUtil; +import net.runelite.client.plugins.microbot.util.reachable.Rs2Reachable; import net.runelite.client.ui.overlay.Overlay; import net.runelite.client.ui.overlay.OverlayLayer; import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayUtil; + import javax.inject.Inject; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics2D; +import java.awt.*; +import java.util.ArrayList; import java.util.List; public class ExampleScriptOverlay extends Overlay { + private static final Color REACHABLE_COLOR = new Color(0, 255, 0, 50); + private static final Color REACHABLE_BORDER_COLOR = new Color(0, 255, 0, 150); + + @Inject + private Client client; + @Inject public ExampleScriptOverlay() { setPosition(OverlayPosition.DYNAMIC); @@ -20,7 +33,56 @@ public ExampleScriptOverlay() { @Override public Dimension render(Graphics2D graphics) { + if (!Microbot.isLoggedIn()) + { + return null; + } + + WorldPoint playerLocation = WorldPoint.fromLocalInstance(client, client.getLocalPlayer().getLocalLocation()); + if (playerLocation == null) + { + return null; + } + + // Get reachable tiles as packed ints + var reachablePacked = Rs2Reachable.getReachableTiles(playerLocation); + if (reachablePacked == null || reachablePacked.isEmpty()) + { + return null; + } + + // Convert packed ints back to world points in current plane/instance + List reachableWorldPoints = new ArrayList<>(); + for (int packed : reachablePacked) + { + WorldPoint wp = WorldPointUtil.unpackWorldPoint(packed); + if (wp.getPlane() == playerLocation.getPlane()) + { + reachableWorldPoints.add(wp); + } + } + + for (WorldPoint worldPoint : reachableWorldPoints) + { + LocalPoint localPoint = LocalPoint.fromWorld(client, worldPoint); + if (localPoint == null) + { + continue; + } + + Polygon poly = Perspective.getCanvasTilePoly(client, localPoint); + if (poly == null) + { + continue; + } + + graphics.setColor(REACHABLE_COLOR); + graphics.fillPolygon(poly); + + graphics.setColor(REACHABLE_BORDER_COLOR); + graphics.drawPolygon(poly); + } + return null; } } - diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java index d30a6b8e4e4..b9f4289ea13 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java @@ -24,6 +24,7 @@ */ package net.runelite.client.plugins.microbot.externalplugins; +import com.fasterxml.jackson.databind.annotation.NoClass; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.graph.Graph; @@ -357,7 +358,11 @@ public void loadSideLoadPlugins() { if (loadedInternalNames.contains(internalName)) { continue; } - loadSideLoadPlugin(internalName); + try { + loadSideLoadPlugin(internalName); + } catch (Exception exception) { + System.out.println("Error loading side-loaded plugin: " + internalName); + } } eventBus.post(new ExternalPluginsChanged()); } @@ -523,9 +528,23 @@ private Plugin instantiate(Collection scannedPlugins, Class claz binder.install(plugin); }; Injector pluginInjector = parent.createChildInjector(pluginModule); + System.out.println(pluginInjector.getClass().getSimpleName()); plugin.setInjector(pluginInjector); - } catch (CreationException ex) { - log.error(ex.getMessage()); + } catch (com.google.common.util.concurrent.ExecutionError e) { + // Guice/Guava wraps NoClassDefFoundError here + Throwable cause = e.getCause(); + if (cause instanceof NoClassDefFoundError) { + log.error("Missing class while loading plugin {}: {}", clazz.getSimpleName(), cause.toString()); + } else { + log.error("Error while loading plugin {}: {}", clazz.getSimpleName(), e.toString(), e); + } + + File jar = getPluginJarFile(plugin.getClass().getSimpleName()); + if (jar != null) { + jar.delete(); + } + } catch (Exception ex) { + log.error("Incompatible plugin found: " + clazz.getSimpleName()); File jar = getPluginJarFile(plugin.getClass().getSimpleName()); jar.delete(); } @@ -562,9 +581,7 @@ private static boolean isMicrobotRelatedPlugin(Class> clazz) { || pkg.contains(".ui") || pkg.contains(".util") || pkg.contains(".shortestpath") - || pkg.contains(".rs2cachedebugger") || pkg.contains(".questhelper") - || pkg.contains("pluginscheduler") || pkg.contains("inventorysetups") || pkg.contains("breakhandler"); } @@ -1061,7 +1078,7 @@ public void remove(MicrobotPluginManifest manifest) { } clearInstalledPluginVersion(internalName); - log.info("Added plugin {} to installed list", manifest.getDisplayName()); + log.info("Removed plugin {} from installed list", manifest.getDisplayName()); eventBus.post(new ExternalPluginsChanged()); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerConfig.java deleted file mode 100644 index d9468cf4f9b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerConfig.java +++ /dev/null @@ -1,336 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler; - -import net.runelite.client.config.Config; -import net.runelite.client.config.ConfigGroup; -import net.runelite.client.config.ConfigItem; -import net.runelite.client.config.ConfigSection; -import net.runelite.client.config.Range; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.antiban.enums.PlaySchedule; - -@ConfigGroup(SchedulerPlugin.configGroup) -public interface SchedulerConfig extends Config { - final static String CONFIG_GROUP = SchedulerPlugin.configGroup; - - @ConfigSection( - name = "Control", - description = "Control settings for the plugin scheduler, force stop, etc.", - position = 10, - closedByDefault = true - ) - String controlSection = "Control Settings"; - @ConfigSection( - name = "Conditions", - description = "Conditions settings for the plugin scheduler, enforce conditions, etc.", - position = 100, - closedByDefault = true - ) - String conditionsSection = "Conditions Settings"; - @ConfigSection( - name = "Log-In", - description = "Log-In settings for the plugin scheduler, auto log in, etc.", - position = 200, - closedByDefault = true - ) - String loginLogOutSection = "Log-In\\Out Settings"; - - @ConfigSection( - name = "Break Between Schedules", - description = "Break Between Schedules settings for the plugin scheduler, auto-enable break handler, etc.", - position = 300, - closedByDefault = true - ) - String breakSection = "Break Settings"; - // hidden settings for saving config automatically via runelite config menager - @ConfigItem( - keyName = "scheduledPlugins", - name = "Scheduled Plugins", - description = "JSON representation of scheduled scripts", - hidden = true - ) - - default String scheduledPlugins() { - return ""; - } - - void setScheduledPlugins(String json); - - // UI Settings - @ConfigItem( - keyName = "showOverlay", - name = "Show Info Overlay", - description = "Show a concise in-game overlay with scheduler status information", - position = 1 - ) - default boolean showOverlay() { - return false; - } - - /// Control settings - @Range( - min = 60, - max = 3600 - ) - @ConfigItem( - keyName = "softStopRetrySeconds", - name = "Soft Stop Retry (seconds)", - description = "Time in seconds between soft stop retry attempts", - position = 1, - section = controlSection - ) - default int softStopRetrySeconds() { - return 60; - } - @ConfigItem( - keyName = "enableHardStop", - name = "Enable Hard Stop", - description = "Enable hard stop after soft stop attempts", - position = 2, - section = controlSection - ) - default boolean enableHardStop() { - return false; - } - void setEnableHardStop(boolean enable); - @ConfigItem( - keyName = "hardStopTimeoutSeconds", - name = "Hard Stop Timeout (seconds)", - description = "Time in seconds before forcing a hard stop after initial soft stop attempt", - position = 3, - section = controlSection - ) - default int hardStopTimeoutSeconds() { - return 0; - } - default void setHardStopTimeoutSeconds(int seconds){ - if (Microbot.getConfigManager() == null){ - return; - } - Microbot.getConfigManager().setConfiguration(CONFIG_GROUP, "hardStopTimeoutSeconds", seconds); - } - - @ConfigItem( - keyName = "minManualStartThresholdMinutes", - name = "Manual Start Threshold (minutes)", - description = "Minimum time (in minutes) to next scheduled plugin, needed so a plugin can be started manually", - position = 4, - section = controlSection - ) - @Range( - min = 1, - max = 60 - ) - default int minManualStartThresholdMinutes() { - return 1; - } - default void setMinManualStartThresholdMinutes(int minutes){ - if (Microbot.getConfigManager() == null){ - return; - } - Microbot.getConfigManager().setConfiguration(CONFIG_GROUP, "minManualStartThresholdMinutes", minutes); - } - @ConfigItem( - keyName = "prioritizeNonDefaultPlugins", - name = "Prioritize Non-Default Plugins", - description = "Stop automatically running default plugins when a non-default plugin is due within the grace period", - position = 5, - section = controlSection - ) - default boolean prioritizeNonDefaultPlugins() { - return true; - } - void setPrioritizeNonDefaultPlugins(boolean prioritizeNonDefaultPlugins); - - @ConfigItem( - keyName = "nonDefaultPluginLookAheadMinutes", - name = "Non-Default Plugin Look-Ahead (minutes)", - description = "Time window in minutes to look ahead for non-default plugins when deciding to stop a default plugin", - position = 6, - section = controlSection - ) - default int nonDefaultPluginLookAheadMinutes() { - return 1; - } - @ConfigItem( - keyName ="notifcationsOn", - name = "Notifications On", - description = "Enable notifications for plugin scheduler events", - position = 7, - section = controlSection - ) - default boolean notificationsOn() { - return false; - } - - - - // Conditions settings - @ConfigItem( - keyName = "enforceTimeBasedStopCondition", - name = "Enforce Stop Conditions", - description = "Prompt for confirmation before running plugins without time based stop conditions", - position = 1, - section = conditionsSection - ) - default boolean enforceTimeBasedStopCondition() { - return true; - } - void setEnforceStopConditions(boolean enforce); - - @ConfigItem( - keyName = "dialogTimeoutSeconds", - name = "Dialog Timeout (seconds)", - description = "Time in seconds before the 'No Stop Conditions' dialog automatically closes", - position = 2, - section = conditionsSection - ) - default int dialogTimeoutSeconds() { - return 30; - } - - @ConfigItem( - keyName = "conditionConfigTimeoutSeconds", - name = "Config Timeout (seconds)", - description = "Time in seconds to wait for a user to add time based stop conditions before canceling plugin start", - position = 3, - section = conditionsSection - ) - default int conditionConfigTimeoutSeconds() { - return 60; - } - - // Log-In settings - @ConfigItem( - keyName = "autoLogIn", - name = "Enable Auto Log In", - description = "Enable auto login before starting the a plugin", - position = 1, - section = loginLogOutSection - ) - default boolean autoLogIn() { - return false; - } - void setAutoLogIn(boolean autoLogIn); - @ConfigItem( - keyName = "autoLogInWorld", - name = "Auto Log In World", - description = "World to log in to, 0 for random world", - position = 2, - section = loginLogOutSection - ) - default int autoLogInWorld() { - return 0; - } - @Range( - min = 0, - max = 2 - ) - @ConfigItem( - keyName = "WorldType", - name = "World Type", - description = "World type to log in to, 0 for F2P, 1 for P2P, 2 for any world", - position = 3, - section = loginLogOutSection - ) - default int worldType() { - return 2; - } - @ConfigItem( - keyName = "autoLogOutOnStop", - name = "Auto Log Out on Stop", - description = "Automatically log out when stopping the scheduler", - position = 3, - section = loginLogOutSection - ) - default boolean autoLogOutOnStop() { - return false; - } - - - // Break settings - @ConfigItem( - keyName = "enableBreakHandlerForSchedule", - name = "Break Handler on Start", - description = "Automatically enable the BreakHandler when starting a plugin", - position = 1, - section = breakSection - ) - default boolean enableBreakHandlerForSchedule() { - return true; - } - - - - @ConfigItem( - keyName = "pauseSchedulerDuringBreak", - name = "Pause the Scheduler during Wait", - description = "During a break, pause the scheduler, all plugins are paused, no progress of starting a plugin", - position = 2, - section = breakSection - ) - default boolean pauseSchedulerDuringBreak() { - return true; - } - @Range( - min = 2, - max = 60 - ) - @ConfigItem( - keyName = "minBreakDuration", - name = "Min Break Duration (minutes)", - description = "The minimum duration of breaks between schedules", - position = 3, - section = breakSection - ) - default int minBreakDuration() { - return 2; - } - @Range( - min = 2, - max = 60 - ) - @ConfigItem( - keyName = "maxBreakDuration", - name = "Max Break Duration (minutes)", - description = "When taking a break, the maximum duration of the break", - position = 4, - section = breakSection - ) - default int maxBreakDuration() { - return 2; - } - @ConfigItem( - keyName = "autoLogOutOnBreak", - name = "Log Out During A Break", - description = "Automatically log out when taking a break", - position = 5, - section = breakSection - ) - default boolean autoLogOutOnBreak() { - return false; - } - - @ConfigItem( - keyName = "usePlaySchedule", - name = "Use Play Schedule", - description = "Enable use of a play schedule to control when the scheduler is active", - position = 6, - section = breakSection - ) - default boolean usePlaySchedule() { - return false; - } - - @ConfigItem( - keyName = "playSchedule", - name = "Play Schedule", - description = "Select the play schedule to use", - position = 7, - section = breakSection - ) - default PlaySchedule playSchedule() { - return PlaySchedule.MEDIUM_DAY; - } - - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerInfoOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerInfoOverlay.java deleted file mode 100644 index 04b9ab1e4cd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerInfoOverlay.java +++ /dev/null @@ -1,345 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler; - -import net.runelite.api.gameval.InterfaceID; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerScript; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.util.SchedulerPluginUtil; -import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; -import net.runelite.client.ui.overlay.OverlayPanel; -import net.runelite.client.ui.overlay.OverlayPosition; -import net.runelite.client.ui.overlay.components.LineComponent; -import net.runelite.client.ui.overlay.components.TitleComponent; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.grandexchange.Rs2GrandExchange; -import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; -import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; - -import javax.inject.Inject; -import java.awt.*; -import java.time.Duration; -import java.util.Optional; - -public class SchedulerInfoOverlay extends OverlayPanel { - private final SchedulerPlugin plugin; - - @Inject - SchedulerInfoOverlay(SchedulerPlugin plugin, SchedulerConfig config) { - super(plugin); - this.plugin = plugin; - setPosition(OverlayPosition.TOP_LEFT); - setNaughty(); - } - - @Override - public Dimension render(Graphics2D graphics) { - try { - if (shouldHideOverlay()) { - return null; - } - panelComponent.setPreferredSize(new Dimension(200, 120)); - - // Title with icon - panelComponent.getChildren().add(TitleComponent.builder() - .text("📅 Plugin Scheduler") - .color(Color.CYAN) - .build()); - - // Current state - SchedulerState currentState = plugin.getCurrentState(); - panelComponent.getChildren().add(LineComponent.builder() - .left("State:") - .right(getStateWithIcon(currentState)) - .rightColor(currentState.getColor()) - .build()); - // Hide overlay when important interfaces are open - - - - // Current plugin info - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - if (currentPlugin != null) { - addCurrentPluginInfo(currentPlugin, currentState); - } else { - panelComponent.getChildren().add(LineComponent.builder() - .left("Current:") - .right("None") - .rightColor(Color.GRAY) - .build()); - } - - // Next plugin info - PluginScheduleEntry nextPlugin = plugin.getUpComingPlugin(); - if (nextPlugin != null) { - addNextPluginInfo(nextPlugin); - } else { - panelComponent.getChildren().add(LineComponent.builder() - .left("Next:") - .right("None scheduled") - .rightColor(Color.GRAY) - .build()); - } - - // Break information (conditionally shown) - addBreakInformation(currentState); - - // Version - panelComponent.getChildren().add(LineComponent.builder().build()); // spacer - panelComponent.getChildren().add(LineComponent.builder() - .left("Version:") - .right(SchedulerPlugin.VERSION) - .rightColor(Color.GRAY) - .build()); - - } catch (Exception ex) { - Microbot.logStackTrace(this.getClass().getSimpleName(), ex); - } - return super.render(graphics); - } - - /** - * Adds current plugin information including progress and time estimates - */ - private void addCurrentPluginInfo(PluginScheduleEntry currentPlugin, SchedulerState currentState) { - String pluginName = currentPlugin.getName(); - if (pluginName.length() > 20) { - pluginName = pluginName.substring(0, 17) + "..."; - } - - panelComponent.getChildren().add(LineComponent.builder() - .left("Current:") - .right(pluginName) - .rightColor(currentState == SchedulerState.RUNNING_PLUGIN ? Color.GREEN : Color.YELLOW) - .build()); - - // Add runtime if running - if (currentPlugin.isRunning()) { - // Calculate current runtime by using lastRunStartTime - Duration runtime = currentPlugin.getLastRunStartTime() != null - ? Duration.between(currentPlugin.getLastRunStartTime(), java.time.ZonedDateTime.now()) - : Duration.ZERO; - panelComponent.getChildren().add(LineComponent.builder() - .left("Runtime:") - .right(formatDuration(runtime)) - .rightColor(Color.WHITE) - .build()); - - // Add stop time estimate if available - Optional stopEstimate = currentPlugin.getEstimatedStopTimeWhenIsSatisfied(); - if (stopEstimate.isPresent()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Est. Stop:") - .right(formatDuration(stopEstimate.get())) - .rightColor(Color.ORANGE) - .build()); - } - - // Add progress percentage if conditions are trackable - double progress = currentPlugin.getStopConditionProgress(); - if (progress > 0) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Progress:") - .right(String.format("%.1f%%", progress)) - .rightColor(getProgressColor(progress)) - .build()); - } - } - } - - /** - * Determines if the overlay should be hidden to avoid interfering with important game interfaces - */ - private boolean shouldHideOverlay() { - try { - return Rs2Bank.isOpen() || Rs2Shop.isOpen() || Rs2GrandExchange.isOpen()|| Rs2Widget.isDepositBoxWidgetOpen() || !Rs2Widget.isHidden(InterfaceID.BankpinKeypad.UNIVERSE); - - } catch (Exception e) { - // Fallback - don't hide if there's an issue checking - return false; - } - } - /** - * Adds next plugin information including time estimates - */ - private void addNextPluginInfo(PluginScheduleEntry nextPlugin) { - String pluginName = nextPlugin.getCleanName(); - if (pluginName.length() > 20) { - pluginName = pluginName.substring(0, 17) + "..."; - } - - panelComponent.getChildren().add(LineComponent.builder() - .left("Next:") - .right(pluginName) - .rightColor(Color.CYAN) - .build()); - - // Add next run time estimate - Optional nextRunEstimate = plugin.getUpComingEstimatedScheduleTime(); - if (nextRunEstimate.isPresent()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Est. Start:") - .right(formatDuration(nextRunEstimate.get())) - .rightColor(Color.LIGHT_GRAY) - .build()); - } else { - // Fallback to plugin's own estimate - Optional pluginEstimate = nextPlugin.getEstimatedStartTimeWhenIsSatisfied(); - if (pluginEstimate.isPresent()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Est. Start:") - .right(formatDuration(pluginEstimate.get())) - .rightColor(Color.LIGHT_GRAY) - .build()); - } - } - } - - /** - * Adds break information, but only shows break countdown if not in micro-breaks-only mode - */ - private void addBreakInformation(SchedulerState currentState) { - // Check if we should show break information - boolean onlyMicroBreaks = getOnlyMicroBreaksConfig(); - boolean showBreakIn = !onlyMicroBreaks || !Rs2AntibanSettings.takeMicroBreaks; - - if (currentState.isBreaking()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Break Status:") - .right("On Break") - .rightColor(Color.YELLOW) - .build()); - - if (BreakHandlerScript.breakDuration > 0) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Break Time:") - .right(formatDuration(Duration.ofSeconds(BreakHandlerScript.breakDuration))) - .rightColor(Color.WHITE) - .build()); - } - } else { - if(SchedulerPluginUtil.isBreakHandlerEnabled() ){ - // Only show break countdown if not in micro-breaks-only mode - if (showBreakIn && BreakHandlerScript.breakIn > 0) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Break In:") - .right(formatDuration(Duration.ofSeconds(BreakHandlerScript.breakIn))) - .rightColor(Color.WHITE) - .build()); - } else if (onlyMicroBreaks && Rs2AntibanSettings.takeMicroBreaks) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Break Mode:") - .right("Micro Breaks Only") - .rightColor(Color.BLUE) - .build()); - } - } - } - } - - /** - * Gets the onlyMicroBreaks configuration value from the BreakHandler config - */ - private boolean getOnlyMicroBreaksConfig() { - try { - Boolean onlyMicroBreaks = Microbot.getConfigManager().getConfiguration( - "break-handler", "OnlyMicroBreaks", Boolean.class); - return onlyMicroBreaks != null && onlyMicroBreaks; - } catch (Exception e) { - // Fallback to false if config cannot be retrieved - return false; - } - } - - /** - * Adds appropriate icon to state display based on current state - */ - private String getStateWithIcon(SchedulerState state) { - String icon; - switch (state) { - case READY: - icon = "✅"; - break; - case SCHEDULING: - icon = "⚙️"; - break; - case STARTING_PLUGIN: - icon = "🚀"; - break; - case RUNNING_PLUGIN: - icon = "▶️"; - break; - case RUNNING_PLUGIN_PAUSED: - case SCHEDULER_PAUSED: - icon = "⏸️"; - break; - case HARD_STOPPING_PLUGIN: - case SOFT_STOPPING_PLUGIN: - icon = "⏹️"; - break; - case HOLD: - icon = "⏳"; - break; - case BREAK: - case PLAYSCHEDULE_BREAK: - icon = "☕"; - break; - case WAITING_FOR_SCHEDULE: - icon = "⌛"; - break; - case WAITING_FOR_LOGIN: - icon = "�"; - break; - case LOGIN: - icon = "�"; - break; - case ERROR: - icon = "❌"; - break; - case INITIALIZING: - icon = "🔄"; - break; - case UNINITIALIZED: - icon = "⚪"; - break; - case WAITING_FOR_STOP_CONDITION: - icon = "⏱️"; - break; - default: - icon = "❓"; - break; - } - return icon + " " + state.getDisplayName(); - } - - /** - * Gets color based on progress percentage - */ - private Color getProgressColor(double progress) { - if (progress < 0.3) { - return Color.RED; - } else if (progress < 0.7) { - return Color.YELLOW; - } else { - return Color.GREEN; - } - } - - /** - * Formats duration in a human-readable format - */ - private String formatDuration(Duration duration) { - if (duration.isZero() || duration.isNegative()) { - return "00:00:00"; - } - - long hours = duration.toHours(); - long minutes = duration.toMinutesPart(); - long seconds = duration.toSecondsPart(); - - if (hours > 0) { - return String.format("%02d:%02d:%02d", hours, minutes, seconds); - } else { - return String.format("%02d:%02d", minutes, seconds); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerPlugin.java deleted file mode 100644 index 14080425cd7..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerPlugin.java +++ /dev/null @@ -1,3667 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler; - -import com.google.inject.Provides; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.GameState; -import net.runelite.api.Skill; -import net.runelite.api.annotations.Component; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.StatChanged; -import net.runelite.client.Notifier; -import net.runelite.client.config.ConfigManager; -import net.runelite.client.config.Notification; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.events.ClientShutdown; -import net.runelite.client.events.ConfigChanged; -import net.runelite.client.events.PluginChanged; -import net.runelite.client.plugins.Plugin; -import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerConfig; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerScript; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.event.ExecutionResult; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryMainTaskFinishedEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPostScheduleTaskFinishedEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPreScheduleTaskFinishedEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry.StopReason; -import net.runelite.client.plugins.microbot.pluginscheduler.serialization.ScheduledSerializer; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.AbstractPrePostScheduleTasks; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState.ExecutionPhase; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.Antiban.AntibanDialogWindow; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.SchedulerPanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.SchedulerWindow; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.util.SchedulerUIUtils; -import net.runelite.client.plugins.microbot.pluginscheduler.util.SchedulerPluginUtil; -import net.runelite.client.plugins.microbot.util.antiban.enums.Activity; -import net.runelite.client.plugins.microbot.util.antiban.enums.ActivityIntensity; -import net.runelite.client.plugins.microbot.util.antiban.enums.CombatSkills; -import net.runelite.client.plugins.microbot.util.cache.Rs2CacheManager; -import net.runelite.client.plugins.microbot.util.events.PluginPauseEvent; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.security.LoginManager; -import net.runelite.client.config.ConfigProfile; -import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; -import net.runelite.client.ui.ClientToolbar; -import net.runelite.client.ui.NavigationButton; -import net.runelite.client.ui.overlay.OverlayManager; -import net.runelite.client.util.ImageUtil; - -import javax.inject.Inject; -import javax.swing.Timer; -import javax.swing.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.image.BufferedImage; -import java.io.File; -import java.nio.file.Files; -import java.time.Duration; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import static net.runelite.client.plugins.microbot.util.Global.*; - -@Slf4j -@PluginDescriptor(name = PluginDescriptor.Mocrosoft + PluginDescriptor.VOX - + "Plugin Scheduler", description = "Schedule plugins at your will", tags = { "microbot", "schedule", - "automation" }, enabledByDefault = false,priority = false) -public class SchedulerPlugin extends Plugin { - public static final String VERSION = "0.1.0"; - @Inject - private SchedulerConfig config; - final static String configGroup = "PluginScheduler"; - - // Store the original break handler logout setting - private Boolean savedBreakHandlerLogoutSetting = null; - private int savedBreakHandlerMaxBreakTime = -1; - private int savedBreakHandlerMinBreakTime = -1; - private Boolean savedOnlyMicroBreaks = null; - private Boolean savedLogout = null; - private volatile boolean isMonitoringPluginStart = false; - @Provides - public SchedulerConfig provideConfig(ConfigManager configManager) { - if (configManager == null) { - return null; - } - return configManager.getConfig(SchedulerConfig.class); - } - - @Inject - private ClientToolbar clientToolbar; - @Inject - private ScheduledExecutorService executorService; - @Inject - private OverlayManager overlayManager; - - private NavigationButton navButton; - private SchedulerPanel panel; - private ScheduledFuture> updateTask; - private SchedulerWindow schedulerWindow; - @Inject - private SchedulerInfoOverlay overlay; - @Getter - private PluginScheduleEntry currentPlugin; - @Getter - private PluginScheduleEntry lastPlugin; - private void setCurrentPlugin(PluginScheduleEntry plugin) { - // Update last plugin when setting new one - if (this.currentPlugin != null && plugin != this.currentPlugin) { - this.lastPlugin = this.currentPlugin; - } - this.currentPlugin = plugin; - } - - /** - * Returns the list of scheduled plugins - * @return List of PluginScheduleEntry objects - */ - @Getter - private List scheduledPlugins = new ArrayList<>(); - - // private final Map nextPluginCache = new - // HashMap<>(); - - private int initCheckCount = 0; - private static final int MAX_INIT_CHECKS = 10; - - @Getter - private SchedulerState currentState = SchedulerState.UNINITIALIZED; - private SchedulerState prvState = SchedulerState.UNINITIALIZED; - private GameState lastGameState = GameState.UNKNOWN; - - // Activity and state tracking - private final Map skillExp = new EnumMap<>(Skill.class); - private Skill lastSkillChanged; - private Instant lastActivityTime = Instant.now(); - private Instant loginTime; - private Activity currentActivity; - private ActivityIntensity currentIntensity; - @Getter - private int idleTime = 0; - // Break tracking - private Duration currentBreakDuration = Duration.ZERO; - private Duration timeUntilNextBreak = Duration.ZERO; - private Optional breakStartTime = Optional.empty(); - // login tracking - private Thread loginMonitor; - private boolean hasDisabledQoLPlugin = false; - @Inject - private Notifier notifier; - - // UI update throttling - private long lastPanelUpdateTime = 0; - private static final long PANEL_UPDATE_THROTTLE_MS = 500; // Minimum 500ms between panel updates - @Override - protected void startUp() { - hasDisabledQoLPlugin=false; - panel = new SchedulerPanel(this); - - final BufferedImage icon = ImageUtil.loadImageResource(SchedulerPlugin.class, "calendar-icon.png"); - navButton = NavigationButton.builder() - .tooltip("Plugin Scheduler") - .priority(10) - .icon(icon) - .panel(panel) - .build(); - - clientToolbar.addNavigation(navButton); - - // Enable overlay if configured - if (config.showOverlay()) { - overlayManager.add(overlay); - } - - // Load saved schedules from config - - // Check initialization status before fully enabling scheduler - //checkInitialization(); - - // Run the main loop - updateTask = executorService.scheduleWithFixedDelay(() -> { - SwingUtilities.invokeLater(() -> { - // Only run scheduling logic if fully initialized - if (currentState.isSchedulerActive()) { - checkSchedule(); - } else if (currentState == SchedulerState.INITIALIZING - || currentState == SchedulerState.UNINITIALIZED) { - // Retry initialization check if not already checking - checkInitialization(); - } - updatePanels(); - }); - }, 0, 1, TimeUnit.SECONDS); - } - - /** - * Checks if all required plugins are loaded and initialized. - * This runs until initialization is complete or max check count is reached. - */ - private void checkInitialization() { - if (!currentState.isInitializing()) { - return; - } - if (Microbot.getClientThread() == null || Microbot.getClient() == null) { - return; - } - setState(SchedulerState.INITIALIZING); - // Schedule repeated checks until initialized or max checks reached - Microbot.getClientThread().invokeLater(() -> { - SchedulerPluginUtil.disableAllRunningNonEessentialPlugin(); - // Find all plugins implementing ConditionProvider - List conditionProviders = Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .collect(Collectors.toList()); - boolean isAtLoginScreen = Microbot.getClient().getGameState() == GameState.LOGIN_SCREEN; - boolean isLoggedIn = Microbot.getClient().getGameState() == GameState.LOGGED_IN; - boolean isAtLoginAuth = Microbot.getClient().getGameState() == GameState.LOGIN_SCREEN_AUTHENTICATOR; - // If any conditions met, mark as initialized - if (isAtLoginScreen || isLoggedIn || isAtLoginAuth) { - log.debug("\nScheduler initialization complete \n\t-{} stopping condition providers loaded", - conditionProviders.size()); - - loadScheduledPluginEntires(); - for (Plugin plugin : conditionProviders) { - try { - - Microbot.stopPlugin(plugin); - } catch (Exception e) { - } - } - setState(SchedulerState.READY); - - // Initial cleanup of one-time plugins after loading - cleanupCompletedOneTimePlugins(); - } - // If max checks reached, mark as initialized but log warning - else if (++initCheckCount >= MAX_INIT_CHECKS) { - log.warn("Scheduler initialization timed out"); - loadScheduledPluginEntires(); - - setState(SchedulerState.ERROR); - } - // Otherwise, schedule another check - else { - log.info("\n\tWaiting for initialization: loginScreen= {}, providers= {}/{}, checks= {}/{}", - isAtLoginScreen, - conditionProviders.stream().count(), - conditionProviders.size(), - initCheckCount, - MAX_INIT_CHECKS); - setState(SchedulerState.INITIALIZING); - checkInitialization(); - } - }); - - } - - public void openSchedulerWindow() { - if (schedulerWindow == null) { - schedulerWindow = new SchedulerWindow(this); - } - - if (!schedulerWindow.isVisible()) { - schedulerWindow.setVisible(true); - } else { - schedulerWindow.toFront(); - schedulerWindow.requestFocus(); - } - } - - @Override - protected void shutDown() { - saveScheduledPlugins(); - clientToolbar.removeNavigation(navButton); - overlayManager.remove(overlay); - forceStopCurrentPluginScheduleEntry(true); - interruptBreak(); - PluginPauseEvent.setPaused(false); - for (PluginScheduleEntry entry : scheduledPlugins) { - entry.close(); - } - if (this.loginMonitor != null && this.loginMonitor.isAlive()) { - this.loginMonitor.interrupt(); - this.loginMonitor = null; - } - if (updateTask != null) { - updateTask.cancel(false); - updateTask = null; - } - - if (schedulerWindow != null) { - schedulerWindow.dispose(); // This will stop the timer - schedulerWindow = null; - } - setState(SchedulerState.UNINITIALIZED); - this.lastGameState = GameState.UNKNOWN; - } - - /** - * Starts the scheduler - */ - public void startScheduler() { - log.info("Starting scheduler request..."); - Microbot.getClientThread().runOnClientThreadOptional(() -> { - // If already active, nothing to do - if (currentState.isSchedulerActive()) { - log.info("Scheduler already active"); - return true; - } - // If initialized, start immediately - if (SchedulerState.READY == currentState || currentState == SchedulerState.HOLD) { - setState(SchedulerState.SCHEDULING); - log.info("Plugin Scheduler started"); - - // Check schedule immediately when started - SwingUtilities.invokeLater(() -> { - checkSchedule(); - }); - return true; - } - return true; - }); - return; - } - - /** - * Stops the scheduler - */ - public void stopScheduler() { - if (loginMonitor != null && loginMonitor.isAlive()) { - loginMonitor.interrupt(); - } - Microbot.getClientThread().runOnClientThreadOptional(() -> { - if (!currentState.isSchedulerActive()) { - return false; // Already stopped - } - if (isOnBreak()) { - log.info("Stopping scheduler while on break, interrupting break"); - interruptBreak(); - } - setState(SchedulerState.HOLD); - log.info("Stopping scheduler..."); - if (currentPlugin != null) { - forceStopCurrentPluginScheduleEntry(true); - } - // Restore the original logout setting if it was stored - if (savedBreakHandlerLogoutSetting != null) { - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Logout", (boolean)savedBreakHandlerLogoutSetting); - log.info("Restored original logout setting: {}", savedBreakHandlerLogoutSetting); - savedBreakHandlerLogoutSetting = null; // Clear the stored value - - } - if (savedBreakHandlerMaxBreakTime != -1) { - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Max BreakTime", (int)savedBreakHandlerMaxBreakTime); - savedBreakHandlerMaxBreakTime = -1; // Clear the stored value - } - if (savedBreakHandlerMinBreakTime != -1) { - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Min BreakTime", (int)savedBreakHandlerMinBreakTime); - savedBreakHandlerMinBreakTime = -1; // Clear the stored value - } - if (savedOnlyMicroBreaks != null) { - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "OnlyMicroBreaks", (boolean)savedOnlyMicroBreaks); - savedOnlyMicroBreaks = null; // Clear the stored value - } - if (savedLogout != null) { - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Logout", (boolean)savedLogout); - savedLogout = null; // Clear the stored value - } - - // Final state after fully stopped, disable the plugins we auto-enabled - if (SchedulerPluginUtil.isBreakHandlerEnabled() && config.enableBreakHandlerForSchedule()) { - if (SchedulerPluginUtil.disableBreakHandler()) { - log.info("Automatically disabled BreakHandler plugin"); - } - } - if (hasDisabledQoLPlugin){ - // SchedulerPluginUtil.enablePlugin(QoLPlugin.class); - } - - setState(SchedulerState.HOLD); - if(config.autoLogOutOnStop()){ - logout(); - } - - log.info("Scheduler stopped - status: {}", currentState); - return false; - }); - } - private boolean checkBreakAndLoginStatus() { - if (currentPlugin!=null){ - if ( !isOnBreak() - && (currentState.isBreaking()) - && prvState == SchedulerState.RUNNING_PLUGIN) { - log.info("Plugin '{}' is paused, but break is not active, resuming plugin", - currentPlugin.getCleanName()); - setState(SchedulerState.RUNNING_PLUGIN); // Ensure the break state is set before pausing - currentPlugin.resume(); - if(config.pauseSchedulerDuringBreak() || allPluginEntryPaused()){ - log.info("\n---config for pause scheduler during break is enabled, resuming all scheduled plugins"); - resumeAllScheduledPlugins(); - } - return true; // Do not continue checking schedule if plugin is running - - }else if (isOnBreak() && currentState == SchedulerState.RUNNING_PLUGIN - ) { - log.info("Plugin '{}' is running, but break is active, pausing plugin", - currentPlugin.getCleanName()); - setState(SchedulerState.BREAK); // Ensure the break state is set before pausing - if (config.pauseSchedulerDuringBreak() || allPluginEntryPaused()) { - log.info("\n---config for pause scheduler during break is enabled, pausing all scheduled plugins"); - pauseAllScheduledPlugins(); - } - currentPlugin.pause(); - - return true; // Do not continue checking schedule if plugin is running - }else if (currentPlugin.isRunning() && currentState.isPluginRunning()) { - - if (!Microbot.isLoggedIn() && !Microbot.isHopping()){ - if (!isOnBreak()){ - //disconnected from the game,--> we are not on break - if (!isAutoLoginEnabled() ){ - log.info("Plugin '{}' is running, but not logged in and auto-login is disabled, starting login monitoring", - currentPlugin.getCleanName()); - startLoginMonitoringThread(); - return true; // If not logged in, wait for login - }else if (isAutoLoginEnabled() ) { - log.info(" wait for auto-login to complete for plugin '{}'", - currentPlugin.getCleanName()); - if( (Microbot.pauseAllScripts.get() || PluginPauseEvent.isPaused() ) && !currentState.isPaused()){ - Microbot.pauseAllScripts.set(false); - PluginPauseEvent.setPaused(hasDisabledQoLPlugin); - } - return true; // If not logged in, wait for login - } - }else{ - log.info("Plugin '{}' is running, but on break, break hanlder should handle it", - currentPlugin.getCleanName()); - } - } - } - } - return false; - } - private void checkSchedule() { - // Update break status - if (SchedulerState.LOGIN == currentState || - SchedulerState.WAITING_FOR_LOGIN == currentState || - SchedulerState.HARD_STOPPING_PLUGIN == currentState || - SchedulerState.SOFT_STOPPING_PLUGIN == currentState || - currentState == SchedulerState.HOLD - // Skip if scheduler is paused - ) { // Skip if running plugin is paused - if ((currentPlugin != null && currentPlugin.isRunning()) && - (SchedulerState.HARD_STOPPING_PLUGIN == currentState || - SchedulerState.SOFT_STOPPING_PLUGIN == currentState ) - ) { - monitorPrePostTaskState(); - } - return; - } - - if(checkBreakAndLoginStatus()){ - return; - } - - // First, check if we need to stop the current plugin - if (isScheduledPluginRunning()) { - checkCurrentPlugin(); - - } - - // If no plugin is running, check for scheduled plugins - if (!isScheduledPluginRunning()) { - int minBreakDuration = config.minBreakDuration(); - PluginScheduleEntry nextUpComingPluginPossibleWithInTime = null; - PluginScheduleEntry nextUpComingPluginPossible = getNextScheduledPluginEntry(false, null).orElse(null); - - if (minBreakDuration == 0) { // 0 means no break - minBreakDuration = 1; - nextUpComingPluginPossibleWithInTime = getNextScheduledPluginEntry(true, null).orElse(null); - } else { - minBreakDuration = Math.max(1, minBreakDuration); - // Get the next scheduled plugin within minBreakDuration - nextUpComingPluginPossibleWithInTime = getUpComingPluginWithinTime( - Duration.ofMinutes(minBreakDuration)); - } - - if ( (nextUpComingPluginPossibleWithInTime == null && - nextUpComingPluginPossible != null && - !nextUpComingPluginPossible.hasOnlyTimeConditions() - && !isOnBreak() && !Microbot.isLoggedIn()) ){ - // when the the next possible plugin is not a time condition and we are not logged in - log.info("\n\nLogin required before the next possible plugin{}can run, start login before hand", nextUpComingPluginPossible.getCleanName()); - - startLoginMonitoringThread(); - return; - } - - if (nextUpComingPluginPossibleWithInTime != null - && nextUpComingPluginPossibleWithInTime.getCurrentStartTriggerTime().isPresent() - && (!config.usePlaySchedule() || !config.playSchedule().isOutsideSchedule())) { - boolean nextWithinFlag = false; - - int withinSeconds = Rs2Random.between(15, 30); // is there plugin upcoming within 15-30, than we stop - // the break - - if (nextUpComingPluginPossibleWithInTime.getCurrentStartTriggerTime().isPresent()) { - nextWithinFlag = Duration - .between(ZonedDateTime.now(ZoneId.systemDefault()), - nextUpComingPluginPossibleWithInTime.getCurrentStartTriggerTime().get()) - .compareTo(Duration.ofSeconds(withinSeconds)) < 0; - } else { - if (nextUpComingPluginPossibleWithInTime.isDueToRun()) { - nextWithinFlag = true; - }else { - - } - } - // If we're on a break, interrupt it - - if (isOnBreak() && (nextWithinFlag)) { - log.info("\n\tInterrupting active break to start scheduled plugin: {}", nextUpComingPluginPossibleWithInTime.getCleanName()); - setState(SchedulerState.BREAK); //ensure the break state is set before interrupting - interruptBreak(); - - } - if (currentState.isBreaking() && nextWithinFlag) { - setState(SchedulerState.WAITING_FOR_SCHEDULE); - } - - if (!currentState.isPluginRunning() && !currentState.isAboutStarting()) { - scheduleNextPlugin(); - } else { - if(currentPlugin == null){ - setState(SchedulerState.WAITING_FOR_SCHEDULE); - }else{ - if (!currentPlugin.isRunning() && !currentState.isAboutStarting()) { - setState( SchedulerState.WAITING_FOR_SCHEDULE); - log.info("Plugin is not running, and it not about to start"); - } - checkCurrentPlugin(); - - } - } - } else { - if(config.usePlaySchedule() && config.playSchedule().isOutsideSchedule() && currentState != SchedulerState.PLAYSCHEDULE_BREAK){ - log.info("\n\tOutside play schedule, starting not started break"); - startBreakBetweenSchedules(config.autoLogOutOnBreak(), 1, 2); - }else if(!isOnBreak() && - currentState != SchedulerState.WAITING_FOR_SCHEDULE && - currentState != SchedulerState.MANUAL_LOGIN_ACTIVE && - currentState == SchedulerState.SCHEDULING) { - // If we're not on a break and there's nothing running, take a short break until - // next plugin (but not if manual login is active) - int minDuration = config.minBreakDuration(); - int maxDuration = config.maxBreakDuration(); - if(nextUpComingPluginPossibleWithInTime != null && nextUpComingPluginPossibleWithInTime.getCurrentStartTriggerTime().isPresent()){ - ZonedDateTime nextPluginTriggerTime = null; - nextPluginTriggerTime = nextUpComingPluginPossibleWithInTime.getCurrentStartTriggerTime().get(); - int maxMinIntervall = maxDuration - minDuration; - minBreakDuration = (int) Duration.between(ZonedDateTime.now(ZoneId.systemDefault()), nextPluginTriggerTime).toMinutes() ; - maxDuration = minBreakDuration + maxMinIntervall; - } - - startBreakBetweenSchedules(config.autoLogOutOnBreak(), minDuration, maxDuration); - }else if(currentState != SchedulerState.WAITING_FOR_SCHEDULE && currentState.isBreaking()){ - //make a resume break function when no plugin is upcoming and the left break time is smaller than "threshold" - //currentBreakDuration -> last set break duration type "Duration" - //breakStartTime breakStartTime -> last set break start time type "Optional " - //breakStartTime.get().plus(currentBreakDuration) -> break end time type "ZonedDateTime" - extendBreakIfNeeded(nextUpComingPluginPossibleWithInTime, 30); - } - } - - } - // Clean up completed one-time plugins - cleanupCompletedOneTimePlugins(); - - } - public void resumeBreak() { - if (currentState == SchedulerState.PLAYSCHEDULE_BREAK){ - // If we are in a play schedule break, we need to reset the state, because otherwise we would break agin, because we are still outside the play schedule - Microbot.getConfigManager().setConfiguration(SchedulerPlugin.configGroup, "usePlaySchedule", false); - } - interruptBreak(); - } - /** - * Interrupts an active break to allow a plugin to start - */ - private void interruptBreak() { - if (!isOnBreak() || (!currentState.isPaused() && !currentState.isBreaking())) { - return; - } - this.currentBreakDuration = Duration.ZERO; - breakStartTime = Optional.empty(); - // Set break duration to 0 to end the break - //BreakHandlerScript.breakDuration = 0; - if (!isBreakHandlerEnabled()) { - return; - } - - log.info("Interrupting active break to start scheduled plugin"); - - - - // Also reset the breakNow setting if it was set - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "breakNow", false); - // Set break end now to true to force end the break immediately - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "breakEndNow", true); - - // Restore the original logout setting if it was stored - if (savedBreakHandlerLogoutSetting != null) { - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Logout", savedBreakHandlerLogoutSetting); - log.info("Restored original logout setting: {}", savedBreakHandlerLogoutSetting); - savedBreakHandlerLogoutSetting = null; // Clear the stored value - } - // Restore the original max break time if it was stored - if (savedBreakHandlerMaxBreakTime != -1) { - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Max BreakTime", savedBreakHandlerMaxBreakTime); - savedBreakHandlerMaxBreakTime = -1; // Clear the stored value - } - if (savedBreakHandlerMinBreakTime != -1) { - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Min BreakTime", savedBreakHandlerMinBreakTime); - savedBreakHandlerMinBreakTime = -1; // Clear the stored value - } - if (savedOnlyMicroBreaks != null) { - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "OnlyMicroBreaks", savedOnlyMicroBreaks); - savedOnlyMicroBreaks = null; // Clear the stored value - } - if (savedLogout != null) { - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Logout", savedLogout); - savedLogout = null; // Clear the stored value - } - - // Ensure we're not locked for future breaks - unlockBreakHandler(); - - // Wait a moment for the break to fully end - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - if (BreakHandlerScript.isBreakActive()) { - SwingUtilities.invokeLater(() -> { - log.info("\n\t--Break was not interrupted successfully"); - interruptBreak(); - }); - return; - } - log.info("\n\t--Break interrupted successfully"); - if ( isBreakHandlerEnabled() && !config.enableBreakHandlerForSchedule()) { - if (SchedulerPluginUtil.disableBreakHandler()) { - log.info("Automatically disabled BreakHandler plugin, should not be used for scheduling"); - } - } - if (currentState.isBreaking()) { - // If we were on a break, reset the state to scheduling - log.info("Resetting state after break interruption currentState: {} prev. state {} ", currentState,prvState); - setState(prvState);// before it was SchedulerState.WAITING_FOR_SCHEDULE - } else { - if (!currentState.isPaused()) { - // If we were paused, reset to the previous state - log.info("Resetting state after break interruption currentState: {} prev. state {} ", currentState,prvState); - // Otherwise, set to waiting for schedule - throw new IllegalStateException("Scheduler state is not breaking or paused, cannot reset to SCHEDULING"); - } - - //setState(prvState);// before it was SchedulerState.SCHEDULING - } - } - - /** - * Hard resets all user conditions for all plugins in the scheduler - * @return A list of plugin names that were reset - */ - public List hardResetAllUserConditions() { - List resetPlugins = new ArrayList<>(); - - for (PluginScheduleEntry entry : scheduledPlugins) { - if (entry != null) { - // Get the condition managers from the entry - try { - entry.hardResetConditions(); - } catch (Exception e) { - log.error("Error resetting conditions for plugin " + entry.getCleanName(), e); - } - } - } - - return resetPlugins; - } - /** - * Starts a short break until the next plugin is scheduled to run - */ - private boolean startBreakBetweenSchedules(boolean logout, - int minBreakDurationMinutes, int maxBreakDurationMinutes) { - StringBuilder logBuilder = new StringBuilder(); - - if (!isBreakHandlerEnabled()) { - if (SchedulerPluginUtil.enableBreakHandler()) { - logBuilder.append("\n\tAutomatically enabled BreakHandler plugin"); - } - log.debug(logBuilder.toString()); - return false; - } - - if (BreakHandlerScript.isLockState()) - BreakHandlerScript.setLockState(false); - - PluginScheduleEntry nextUpComingPlugin = getUpComingPlugin(); - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - Duration timeUntilNext = Duration.ZERO; - - // Check if we're outside play schedule - if (config.usePlaySchedule() && config.playSchedule().isOutsideSchedule()) { - Duration untilNextSchedule = config.playSchedule().timeUntilNextSchedule(); - logBuilder.append("\n\tOutside play schedule") - .append("\n\t\tNext schedule in: ").append(formatDuration(untilNextSchedule)); - - // Configure a break until the next play schedule time - BreakHandlerScript.breakDuration = (int) untilNextSchedule.getSeconds(); - this.currentBreakDuration = untilNextSchedule; - BreakHandlerScript.breakIn = 0; - - // Store the original logout setting before changing it - savedBreakHandlerLogoutSetting = Microbot.getConfigManager().getConfiguration( - BreakHandlerConfig.configGroup, "Logout", Boolean.class); - // Set the new logout setting - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Logout", true); - - if (untilNextSchedule.getSeconds() > 60){ - savedBreakHandlerMaxBreakTime = Microbot.getConfigManager().getConfiguration( - BreakHandlerConfig.configGroup, "Max BreakTime", Integer.class); - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Max BreakTime",(int)(untilNextSchedule.toMinutes())); - savedBreakHandlerMinBreakTime = Microbot.getConfigManager().getConfiguration( - BreakHandlerConfig.configGroup, "Min BreakTime", Integer.class); - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Min BreakTime",(int)(untilNextSchedule.toMinutes())); - } - - // Set state to indicate we're in a break - sleepUntil(() -> BreakHandlerScript.isBreakActive(), 1000); - - if (!BreakHandlerScript.isBreakActive()) { - logBuilder.append("\n\t\tWarning: Break handler is not active, unable to start break for play schedule"); - log.info(logBuilder.toString()); - return false; - } - - setState(SchedulerState.PLAYSCHEDULE_BREAK); - log.info(logBuilder.toString()); - return true; - } - - // Store the original logout setting before changing it - savedBreakHandlerLogoutSetting = Microbot.getConfigManager().getConfiguration( - BreakHandlerConfig.configGroup, "Logout", Boolean.class); - // Set the new logout setting - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Logout", logout); - - // Determine the time until the next plugin is scheduled - if (nextUpComingPlugin != null) { - Optional nextStartTime = nextUpComingPlugin.getCurrentStartTriggerTime(); - if (nextStartTime.isPresent()) { - timeUntilNext = Duration.between(now, nextStartTime.get()); - } - } - - // Determine the break duration based on config and next plugin time - long breakSeconds; - - // Calculate a random break duration between min and max - int randomBreakMinutes = Rs2Random.between(minBreakDurationMinutes, maxBreakDurationMinutes); - breakSeconds = randomBreakMinutes * 60; - - logBuilder.append("\n\tStarting break between schedules") - .append("\n\t\tInitial break duration: ").append(formatDuration(Duration.ofSeconds(breakSeconds))); - - // If there's a next plugin scheduled, make sure we don't break past its start time - if (nextUpComingPlugin != null && timeUntilNext.toSeconds() > 0) { - // Subtract 30 seconds buffer to ensure we're back before the plugin needs to start - long maxBreakForNextPlugin = timeUntilNext.toSeconds() - 30; - if (maxBreakForNextPlugin > 60) { // Only consider breaks that would be at least 1 minute - breakSeconds = Math.max(breakSeconds, maxBreakForNextPlugin); - logBuilder.append("\n\t\tLimited break duration to: ").append(formatDuration(Duration.ofSeconds(breakSeconds))) - .append("\n\t\tUpcoming plugin: ").append(nextUpComingPlugin.getCleanName()) - .append(" (in ").append(formatDuration(timeUntilNext)).append(")"); - } - - logBuilder.append("\n\t\tNext plugin scheduled:") - .append("\n\t\t\tTime until next: ").append(formatDuration(timeUntilNext)) - .append("\n\t\t\tNext start time: ").append(nextUpComingPlugin.getCurrentStartTriggerTime().get()) - .append("\n\t\t\tCurrent time: ").append(now); - } - - if (breakSeconds > 0){ - this.savedBreakHandlerMaxBreakTime = Microbot.getConfigManager().getConfiguration( - BreakHandlerConfig.configGroup, "Max BreakTime", Integer.class); - this.savedBreakHandlerMinBreakTime = Microbot.getConfigManager().getConfiguration( - BreakHandlerConfig.configGroup, "Min BreakTime", Integer.class); - - int maxBreakMinutes = (int)(breakSeconds / 60) + 1; - int minBreakMinutes = (int)(breakSeconds / 60); - - logBuilder.append("\n\t\tConfiguring BreakHandler:") - .append("\n\t\t\tMax break time: ").append(maxBreakMinutes).append(" minutes") - .append("\n\t\t\tMin break time: ").append(minBreakMinutes).append(" minutes"); - - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Max BreakTime", maxBreakMinutes); - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Min BreakTime", minBreakMinutes); - } - this.savedOnlyMicroBreaks = Microbot.getConfigManager().getConfiguration( - BreakHandlerConfig.configGroup, "OnlyMicroBreaks", Boolean.class); - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "OnlyMicroBreaks", false); - this.savedLogout = Microbot.getConfigManager().getConfiguration( - BreakHandlerConfig.configGroup, "Logout", Boolean.class); - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "Logout", true); - int currentMaxBreakTimeBreakHandler = Microbot.getConfigManager().getConfiguration( - BreakHandlerConfig.configGroup, "Max BreakTime", Integer.class); - int currentMinBreakTimeBreakHandler = Microbot.getConfigManager().getConfiguration( - BreakHandlerConfig.configGroup, "Min BreakTime", Integer.class); - - logBuilder.append("\n\t\tCurrent break handler settings:") - .append("\n\t\t\tMin: ").append(currentMinBreakTimeBreakHandler).append(" minutes") - .append("\n\t\t\tMax: ").append(currentMaxBreakTimeBreakHandler).append(" minutes") - .append("\n\t\t\tLogout: ").append(this.savedBreakHandlerLogoutSetting) - .append("\n\t\t\tOnlyMicroBreaks: ").append(this.savedOnlyMicroBreaks); - - - if (breakSeconds < 60) { - // Break would be too short, don't take one - logBuilder.append("\n\t\tNot taking break - duration would be less than 1 minute"); - savedBreakHandlerLogoutSetting = null; // Clear the stored value - log.info(logBuilder.toString()); - return false; - } - - logBuilder.append("\n\t\tFinal break duration: ").append(formatDuration(Duration.ofSeconds(breakSeconds))); - - // Configure the break - BreakHandlerScript.breakDuration = (int) breakSeconds; - this.currentBreakDuration = Duration.ofSeconds(breakSeconds); - BreakHandlerScript.breakIn = 0; - - // Set state to indicate we're in a break - sleepUntil(() -> BreakHandlerScript.isBreakActive(), 1000); - - if (!BreakHandlerScript.isBreakActive()) { - logBuilder.append("\n\t\tError: Break handler is not active, unable to start break"); - log.info(logBuilder.toString()); - return false; - } - - setState(SchedulerState.BREAK); - logBuilder.append("\n\t\tBreak successfully started"); - - log.debug(logBuilder.toString()); - return true; - } - - /** - * Format a duration for display - */ - private String formatDuration(Duration duration) { - return SchedulerPluginUtil.formatDuration(duration); - } - - /** - * Schedules the next plugin to run if none is running - */ - private void scheduleNextPlugin() { - // Check if a non-default plugin is coming up soon - boolean prioritizeNonDefaultPlugins = config.prioritizeNonDefaultPlugins(); - int nonDefaultPluginLookAheadMinutes = config.nonDefaultPluginLookAheadMinutes(); - - if (prioritizeNonDefaultPlugins) { - // Look for any upcoming non-default plugin within the configured time window - PluginScheduleEntry upcomingNonDefault = getNextScheduledPluginEntry(false, - Duration.ofMinutes(nonDefaultPluginLookAheadMinutes)) - .filter(plugin -> !plugin.isDefault()) - .orElse(null); - - // If we found an upcoming non-default plugin, check if it's already due to run - if (upcomingNonDefault != null && !upcomingNonDefault.isDueToRun()) { - // Get the next plugin that's due to run now - Optional nextDuePlugin = getNextScheduledPluginEntry(true, null); - - // If the next due plugin is a default plugin, don't start it - // Instead, wait for the non-default plugin - if (nextDuePlugin.isPresent() && nextDuePlugin.get().isDefault()) { - log.info("\nNot starting default plugin '{}' because non-default plugin '{}' is scheduled within {}[configured] minutes", - nextDuePlugin.get().getCleanName(), - upcomingNonDefault.getCleanName(), - nonDefaultPluginLookAheadMinutes); - return; - } - } - } - - // Get the next plugin that's due to run - Optional selected = getNextScheduledPluginEntry(true, null); - if (selected.isEmpty()) { - return; - } - - // If we're on a break, interrupt it, only we have initialized the break - if (isOnBreak() && currentBreakDuration != null && currentBreakDuration.getSeconds() > 0) { - log.info("\nInterrupting active break to start scheduled plugin: \n\t{}", selected.get().getCleanName()); - interruptBreak(); - } - - - log.info("\nStarting scheduled plugin: \n\t{}\ncurrent state \n\t{}", selected.get().getCleanName(),this.currentState); - - // reset manual login state when starting a new plugin to restore automatic break handling - resetManualLoginState(); - - startPluginScheduleEntry(selected.get()); - if (!selected.get().isRunning()) { - saveScheduledPlugins(); - } - } - - - public void startPluginScheduleEntry(PluginScheduleEntry scheduledPlugin) { - - Microbot.getClientThread().runOnClientThreadOptional(() -> { - - if (scheduledPlugin == null) - return false; - // Ensure BreakHandler is enabled when we start a plugin - if (!SchedulerPluginUtil.isBreakHandlerEnabled() && config.enableBreakHandlerForSchedule()) { - log.info("Start enabling BreakHandler plugin"); - if (SchedulerPluginUtil.enableBreakHandler()) { - log.info("Automatically enabled BreakHandler plugin"); - } - } - - // Ensure Antiban is enabled when we start a plugin -> should be allways - // enabled? - if (!SchedulerPluginUtil.isAntibanEnabled()) { - log.info("Start enabling Antiban plugin"); - if (SchedulerPluginUtil.enableAntiban()) { - log.info("Automatically enabled Antiban plugin"); - } - } - // Ensure QoL is disabled when we start a plugin - /*if (SchedulerPluginUtil.isPluginEnabled(QoLPlugin.class)) { - log.info("Disabling QoL plugin"); - if (SchedulerPluginUtil.disablePlugin(QoLPlugin.class)) { - hasDisabledQoLPlugin = true; - log.info("Automatically disabled QoL plugin"); - } - }*/ - - // Ensure break handler is unlocked before starting a plugin - SchedulerPluginUtil.unlockBreakHandler(); - - // If we're on a break, interrupt it - if (isOnBreak()) { - interruptBreak(); - } - SchedulerState stateBeforeScheduling = currentState; - setCurrentPlugin(scheduledPlugin); - - - // Check for stop conditions if enforcement is enabled -> ensure we have stop - // condition so the plugin doesn't run forever (only manual stop possible - // otherwise) - if ( config.enforceTimeBasedStopCondition() - && scheduledPlugin.isNeedsStopCondition() - && scheduledPlugin.getStopConditionManager().getUserTimeConditions().isEmpty() - && SchedulerState.SCHEDULING == currentState) { - // If the user chooses to add stop conditions, we wait for them to be added - // and then continue the scheduling process - // If the user chooses not to add stop conditions, we proceed with the plugin - // start - // If the user cancels, we reset the state and do not start the plugin - // Show confirmation dialog on EDT to prevent blocking - // Start the dialog in a separate thread to avoid blocking the EDT - setState(SchedulerState.WAITING_FOR_STOP_CONDITION); - startAddStopConditionDialog(scheduledPlugin, stateBeforeScheduling); - log.info("No stop conditions set for plugin: " + scheduledPlugin.getCleanName()); - return false; - } else { - if (currentState != SchedulerState.STARTING_PLUGIN){ - setState(SchedulerState.STARTING_PLUGIN); - // Stop conditions exist or enforcement disabled - proceed normally - monitorStartingPluginScheduleEntry(scheduledPlugin); - } - return true; - } - }); - } - - private void startAddStopConditionDialog(PluginScheduleEntry scheduledPlugin, - SchedulerState stateBeforeScheduling) { - // Show confirmation dialog on EDT to prevent blocking - Microbot.getClientThread().runOnSeperateThread(() -> { - // Create dialog with timeout - final JOptionPane optionPane = new JOptionPane( - "Plugin '" + scheduledPlugin.getCleanName() + "' has no stop time based conditions set.\n" + - "It will run until manually stopped or a other condition (when defined).\n\n" + - "Would you like to configure stop conditions now?", - JOptionPane.QUESTION_MESSAGE, - JOptionPane.YES_NO_CANCEL_OPTION); - - final JDialog dialog = optionPane.createDialog("No Stop Conditions"); - - // Create timer for dialog timeout - int timeoutSeconds = config.dialogTimeoutSeconds(); - if (timeoutSeconds <= 0) { - timeoutSeconds = 30; // Default timeout if config value is invalid - } - - final Timer timer = new Timer(timeoutSeconds * 1000, e -> { - dialog.setVisible(false); - dialog.dispose(); - }); - timer.setRepeats(false); - timer.start(); - - // Update dialog title to show countdown - final int finalTimeoutSeconds = timeoutSeconds; - final Timer countdownTimer = new Timer(1000, new ActionListener() { - int remainingSeconds = finalTimeoutSeconds; - - @Override - public void actionPerformed(ActionEvent e) { - remainingSeconds--; - if (remainingSeconds > 0) { - dialog.setTitle("No Stop Conditions (Timeout: " + remainingSeconds + "s)"); - } else { - dialog.setTitle("No Stop Conditions (Timing out...)"); - } - } - }); - countdownTimer.start(); - - try { - dialog.setVisible(true); // blocks until dialog is closed or timer expires - } finally { - timer.stop(); - countdownTimer.stop(); - } - - // Handle user choice or timeout - Object selectedValue = optionPane.getValue(); - int result = selectedValue instanceof Integer ? (Integer) selectedValue : JOptionPane.CLOSED_OPTION; - log.info("User selected: " + result); - if (result == JOptionPane.YES_OPTION) { - // User wants to add stop conditions - openSchedulerWindow(); - if (schedulerWindow != null) { - // Switch to stop conditions tab - schedulerWindow.selectPlugin(scheduledPlugin); - schedulerWindow.switchToStopConditionsTab(); - schedulerWindow.toFront(); - - // Start a timer to check if conditions have been added - int conditionTimeoutSeconds = config.conditionConfigTimeoutSeconds(); - if (conditionTimeoutSeconds <= 0) { - conditionTimeoutSeconds = 60; // Default if config value is invalid - } - - final Timer conditionTimer = new Timer(conditionTimeoutSeconds * 1000, evt -> { - // Check if any time conditions have been added - if (scheduledPlugin.getStopConditionManager().getConditions().isEmpty()) { - log.info("No conditions added within timeout period. Returning to previous state."); - setCurrentPlugin(null); - setState(stateBeforeScheduling); - - SwingUtilities.invokeLater(() -> { - JOptionPane.showMessageDialog( - schedulerWindow, - "No time conditions were added within the timeout period.\n" + - "Plugin start has been canceled.", - "Configuration Timeout", - JOptionPane.WARNING_MESSAGE); - }); - }else{ - // Stop the timer if conditions are added - - log.info("Stop conditions added successfully for plugin: " + scheduledPlugin.getCleanName()); - setState(SchedulerState.STARTING_PLUGIN); - monitorStartingPluginScheduleEntry(scheduledPlugin); - } - }); - conditionTimer.setRepeats(false); - conditionTimer.start(); - } - } else if (result == JOptionPane.NO_OPTION) { - setState(SchedulerState.STARTING_PLUGIN); - // User confirms to run without stop conditions - monitorStartingPluginScheduleEntry(scheduledPlugin); - scheduledPlugin.setNeedsStopCondition(false); - log.info("User confirmed to run plugin without stop conditions: {}", scheduledPlugin.getCleanName()); - } else { - // User canceled or dialog timed out - abort starting - log.info("Plugin start canceled by user or timed out: {}", scheduledPlugin.getCleanName()); - scheduledPlugin.setNeedsStopCondition(false); - setCurrentPlugin(null); - setState(stateBeforeScheduling); - } - return null; - }); - } - - /** - * Resets any pending plugin start operation - */ - public void resetPendingStart() { - if (currentState == SchedulerState.STARTING_PLUGIN || currentState == SchedulerState.WAITING_FOR_LOGIN || - currentState == SchedulerState.WAITING_FOR_STOP_CONDITION) { - setCurrentPlugin(null); - - setState(SchedulerState.SCHEDULING); - } - } - public void continuePendingStart(PluginScheduleEntry scheduledPlugin) { - if (currentState == SchedulerState.WAITING_FOR_STOP_CONDITION ) { - if (currentPlugin != null && !currentPlugin.isRunning() && currentPlugin.equals(scheduledPlugin)) { - setState(SchedulerState.STARTING_PLUGIN); - log.info("Continuing pending start for plugin: " + scheduledPlugin.getCleanName()); - this.monitorStartingPluginScheduleEntry(scheduledPlugin); - } - } - } - /** - * Continues the plugin starting process after stop condition checks - */ - private void monitorStartingPluginScheduleEntry(PluginScheduleEntry scheduledPlugin) { - if (isMonitoringPluginStart) { - log.debug("Already monitoring plugin start, skipping duplicate call"); - return; - } - isMonitoringPluginStart = true; - try { - if (scheduledPlugin == null || (currentState != SchedulerState.STARTING_PLUGIN && currentState != SchedulerState.LOGIN)) { - log.info("No plugin to start or not in STARTING_PLUGIN state, resetting state to SCHEDULING"); - setCurrentPlugin(null); - setState(SchedulerState.SCHEDULING); - return; - } - Microbot.getClientThread().runOnClientThreadOptional(() -> { - if (scheduledPlugin.isRunning()) { - log.info("\n\tPlugin started successfully: " + scheduledPlugin.getCleanName()); - - // Stop startup watchdog since plugin successfully transitioned to running state - scheduledPlugin.stopStartupWatchdog(); - - - // Check if plugin implements SchedulablePlugin and trigger pre-schedule tasks - Plugin plugin = scheduledPlugin.getPlugin(); - if (plugin instanceof SchedulablePlugin) { - log.info("Plugin '{}' implements SchedulablePlugin - triggering pre-schedule tasks", scheduledPlugin.getCleanName()); - - AbstractPrePostScheduleTasks prePostScheduleTasks = getCurrentPluginPrePostTasks(); - if (prePostScheduleTasks != null) { - if(!prePostScheduleTasks.getRequirements().isInitialized()){ - log.warn("we have a pre post schedule tasks, but the requirements are not initialized, initializing them now. we wait for the plugin to be fully started"); - Microbot.getClientThread().invokeLater( ()->{ - monitorStartingPluginScheduleEntry(scheduledPlugin); - }); - return false; - } - } - // Trigger pre-schedule tasks after a short delay to ensure plugin is fully subscribed to EventBus - Microbot.getClientThread().invokeLater(() -> { - boolean triggered = scheduledPlugin.triggerPreScheduleTasks(); - if (triggered) { - log.info("Pre-schedule tasks triggered successfully for plugin '{}'", scheduledPlugin.getCleanName()); - setState(SchedulerState.EXECUTING_PRE_SCHEDULE_TASKS); - - } else { - log.info("No pre-schedule tasks to trigger for plugin '{}' - proceeding to running state", scheduledPlugin.getCleanName()); - setState(SchedulerState.RUNNING_PLUGIN); - } - return true; - }); - } else { - // For non-SchedulablePlugin implementations, proceed directly to running state - setState(SchedulerState.RUNNING_PLUGIN); - } - - // Check if the plugin has started executing pre-schedule tasks - monitorPrePostTaskState(); - return true; - } - if (!Microbot.isLoggedIn()) { - log.info("\n -Login required before running plugin: " + scheduledPlugin.getCleanName()+"\n\tcurrent state: " + currentState + "\n\tprevious state: " + prvState); - startLoginMonitoringThread(); - return false; - } - - // wait for game state to be fully loaded after login - - if(isGameStateFullyLoaded()){ - if(!scheduledPlugin.isHasStarted()){ - if ( !scheduledPlugin.start(false)) { - log.error("Failed to start plugin: " + scheduledPlugin.getCleanName()); - setCurrentPlugin(null); - setState(SchedulerState.SCHEDULING); - return false; - } - }else{ - log.debug("Plugin already started: " + scheduledPlugin.getCleanName()); - } - }else{ - log.debug("\n\tGame state not fully loaded yet for plugin: " + scheduledPlugin.getCleanName() + ", waiting..."); - } - if (currentState != SchedulerState.LOGIN){ - Microbot.getClientThread().invokeLater( ()->{ - monitorStartingPluginScheduleEntry(scheduledPlugin); - }); - } - - return false; - - - }); - } finally { - isMonitoringPluginStart = false; - } - } - - public void forceStopCurrentPluginScheduleEntry(boolean successful) { - if (currentPlugin != null && currentPlugin.isRunning()) { - log.info("Force Stopping current plugin: " + currentPlugin.getCleanName()); - if (currentState == SchedulerState.RUNNING_PLUGIN) { - setState(SchedulerState.HARD_STOPPING_PLUGIN); - } - if (currentPlugin.isPaused()) { - // If the plugin is paused, unpause it first to allow it to stop cleanly - currentPlugin.resume(); - } - currentPlugin.stop(successful, StopReason.HARD_STOP, "Plugin was forcibly stopped by user request"); - // Wait a short time to see if the plugin stops immediately - if (currentPlugin != null) { - - if (!currentPlugin.isRunning()) { - log.info("Plugin stopped successfully: " + currentPlugin.getCleanName()); - - } else { - SwingUtilities.invokeLater(() -> { - forceStopCurrentPluginScheduleEntry(successful); - }); - log.info("Failed to hard stop plugin: " + currentPlugin.getCleanName()); - } - } - } - updatePanels(); - } - - /** - * Update all UI panels with the current state. - * Throttled to prevent excessive refresh calls. - */ - void updatePanels() { - long currentTime = System.currentTimeMillis(); - - // Throttle panel updates to prevent excessive refreshes - if (currentTime - lastPanelUpdateTime < PANEL_UPDATE_THROTTLE_MS) { - return; - } - - lastPanelUpdateTime = currentTime; - - if (panel != null) { - panel.refresh(); - } - - if (schedulerWindow != null && schedulerWindow.isVisible()) { - schedulerWindow.refresh(); - } - } - - /** - * Force immediate update of all UI panels, bypassing throttling. - * Use this for critical state changes that require immediate UI updates. - */ - void forceUpdatePanels() { - if (panel != null) { - panel.refresh(); - } - - if (schedulerWindow != null && schedulerWindow.isVisible()) { - schedulerWindow.refresh(); - } - } - - public void addScheduledPlugin(PluginScheduleEntry plugin) { - scheduledPlugins.add(plugin); - // Register the stop completion callback - registerStopCompletionCallback(plugin); - } - - public void removeScheduledPlugin(PluginScheduleEntry plugin) { - plugin.setEnabled(false); - scheduledPlugins.remove(plugin); - } - - public void updateScheduledPlugin(PluginScheduleEntry oldPlugin, PluginScheduleEntry newPlugin) { - int index = scheduledPlugins.indexOf(oldPlugin); - if (index >= 0) { - scheduledPlugins.set(index, newPlugin); - // Register the stop completion callback for the new plugin - registerStopCompletionCallback(newPlugin); - } - } - - - - /** - * Saves scheduled plugins to a specific file - * - * @param file The file to save to - * @return true if save was successful, false otherwise - */ - public boolean savePluginScheduleEntriesToFile(File file) { - try { - // Convert to JSON - String json = ScheduledSerializer.toJson(scheduledPlugins, SchedulerPlugin.VERSION); - - // Write to file - java.nio.file.Files.writeString(file.toPath(), json); - log.info("Saved scheduled plugins to file: {}", file.getAbsolutePath()); - return true; - } catch (Exception e) { - log.error("Error saving scheduled plugins to file", e); - return false; - } - } - - /** - * Loads scheduled plugins from a specific file - * - * @param file The file to load from - * @return true if load was successful, false otherwise - */ - public boolean loadPluginScheduleEntriesFromFile(File file) { - try { - stopScheduler(); - if(currentPlugin != null && currentPlugin.isRunning()){ - forceStopCurrentPluginScheduleEntry(false); - log.info("Stopping current plugin before loading new schedule"); - } - sleepUntil(() -> (currentPlugin == null || !currentPlugin.isRunning()), 2000); - // Read JSON from file - String json = Files.readString(file.toPath()); - log.info("Loading scheduled plugins from file: {}", file.getAbsolutePath()); - - // Parse JSON - List loadedPlugins = ScheduledSerializer.fromJson(json, this.VERSION); - if (loadedPlugins == null) { - log.error("Failed to parse JSON from file"); - return false; - } - - // Resolve plugin references - for (PluginScheduleEntry entry : loadedPlugins) { - resolvePluginReferences(entry); - // Register stop completion callback - registerStopCompletionCallback(entry); - } - - // Replace current plugins - scheduledPlugins = loadedPlugins; - - // Update UI - SwingUtilities.invokeLater(this::updatePanels); - return true; - } catch (Exception e) { - log.error("Error loading scheduled plugins from file", e); - return false; - } - } - - /** - * Adds stop conditions to a scheduled plugin - */ - public void addUserStopConditionsToPluginScheduleEntry(PluginScheduleEntry plugin, List conditions, - boolean requireAll, boolean stopOnConditionsMet) { - // Call the enhanced version with null file to use default config - addUserConditionsToPluginScheduleEntry(plugin, conditions, null, requireAll, stopOnConditionsMet, null); - } - - /** - * Adds conditions to a scheduled plugin with support for saving to a specific file - * - * @param plugin The plugin to add conditions to - * @param stopConditions List of stop conditions - * @param startConditions List of start conditions (optional, can be null) - * @param requireAll Whether all conditions must be met - * @param stopOnConditionsMet Whether to stop the plugin when conditions are met - * @param saveFile Optional file to save the conditions to, or null to use default config - */ - public void addUserConditionsToPluginScheduleEntry(PluginScheduleEntry plugin, List stopConditions, - List startConditions, boolean requireAll, boolean stopOnConditionsMet, File saveFile) { - if (plugin == null) - return; - - // Clear existing stop conditions - plugin.getStopConditionManager().getUserConditions().clear(); - - // Add new stop conditions - for (Condition condition : stopConditions) { - plugin.addStopCondition(condition); - } - - // Add start conditions if provided - if (startConditions != null) { - plugin.getStartConditionManager().getUserConditions().clear(); - for (Condition condition : startConditions) { - plugin.addStartCondition(condition); - } - } - - // Set condition manager properties - if (requireAll) { - plugin.getStopConditionManager().setRequireAll(); - } else { - plugin.getStopConditionManager().setRequireAny(); - } - - // Save to specified file if provided, otherwise to config - if (saveFile != null) { - savePluginScheduleEntriesToFile(saveFile); - } else { - // Save to config - saveScheduledPlugins(); - } - } - - /** - * Gets the list of plugins that have stop conditions set - */ - public List getScheduledPluginsWithStopConditions() { - return scheduledPlugins.stream() - .filter(p -> !p.getStopConditionManager().getConditions().isEmpty()) - .collect(Collectors.toList()); - } - - /** - * returns all scheduled plugins regardless of their availability status. - * this includes plugins that may not be currently loaded from the plugin hub. - * use this for serialization and complete schedule management. - * - * @return all scheduled plugins including unavailable ones - */ - public List getAllScheduledPlugins() { - return new ArrayList<>(scheduledPlugins); - } - - /** - * returns only the scheduled plugins that are currently available in the plugin manager. - * this filters out plugins that are not loaded, hidden, or don't implement SchedulablePlugin. - * use this for active scheduling operations. - * - * @return only available scheduled plugins - */ - public List getScheduledPlugins() { - return scheduledPlugins.stream() - .filter(PluginScheduleEntry::isPluginAvailable) - .collect(Collectors.toList()); - } - - /** - * returns scheduled plugins that are not currently available. - * useful for debugging and showing which plugins are missing from the hub. - * - * @return unavailable scheduled plugins - */ - public List getUnavailableScheduledPlugins() { - return scheduledPlugins.stream() - .filter(entry -> !entry.isPluginAvailable()) - .collect(Collectors.toList()); - } - - public void saveScheduledPlugins() { - // Convert to JSON and save to config - String json = ScheduledSerializer.toJson(scheduledPlugins, this.VERSION); - if (Microbot.getConfigManager() == null) { - return; - } - Microbot.getConfigManager().setConfiguration(SchedulerPlugin.configGroup, "scheduledPlugins", json); - - } - - private void loadScheduledPluginEntires() { - try { - // Load from config and parse JSON - if (Microbot.getConfigManager() == null) { - return; - } - String json = Microbot.getConfigManager().getConfiguration(SchedulerConfig.CONFIG_GROUP, - "scheduledPlugins"); - log.debug("Loading scheduled plugins from config: {}\n\n", json); - - if (json != null && !json.isEmpty()) { - this.scheduledPlugins = ScheduledSerializer.fromJson(json, this.VERSION); - - // Apply stop settings from config to all loaded plugins - for (PluginScheduleEntry plugin : scheduledPlugins) { - // Set timeout values from config - plugin.setSoftStopRetryInterval(Duration.ofSeconds(config.softStopRetrySeconds())); - plugin.setHardStopTimeout(Duration.ofSeconds(config.hardStopTimeoutSeconds())); - - // Resolve plugin references - resolvePluginReferences(plugin); - - // Register stop completion callback - registerStopCompletionCallback(plugin); - - StringBuilder logMessage = new StringBuilder(); - logMessage.append(String.format("\nLoaded scheduled plugin:\n %s with %d conditions:\n", - plugin.getName(), - plugin.getStopConditionManager().getConditions().size() + plugin.getStartConditionManager().getConditions().size())); - - // Start conditions section - logMessage.append(String.format("\tStart user condition (%d):\n\t\t%s\n", - plugin.getStartConditionManager().getUserLogicalCondition().getTotalConditionCount(), - plugin.getStartConditionManager().getUserLogicalCondition().getDescription())); - logMessage.append(String.format("\tStart plugin conditions (%d):\n\t\t%s", - plugin.getStartConditionManager().getPluginCondition().getTotalConditionCount(), - plugin.getStartConditionManager().getPluginCondition().getDescription())); - // Stop conditions section - logMessage.append(String.format("\tStop user condition (%d):\n\t\t%s\n", - plugin.getStopConditionManager().getUserLogicalCondition().getTotalConditionCount(), - plugin.getStopConditionManager().getUserLogicalCondition().getDescription())); - logMessage.append(String.format("\tStop plugin conditions (%d):\n\t\t%s\n", - plugin.getStopConditionManager().getPluginCondition().getTotalConditionCount(), - plugin.getStopConditionManager().getPluginCondition().getDescription())); - - - - log.debug(logMessage.toString()); - - // Log condition details at debug level - if (Microbot.isDebug()) { - plugin.logConditionInfo(plugin.getStopConditionManager().getConditions(), - "LOADING - Stop Conditions", true); - plugin.logConditionInfo(plugin.getStartConditionManager().getConditions(), - "LOADING - Start Conditions", true); - } - } - - // Force UI update after loading plugins - SwingUtilities.invokeLater(this::updatePanels); - } - } catch (Exception e) { - log.error("Error loading scheduled plugins", e); - this.scheduledPlugins = new ArrayList<>(); - } - } - - /** - * Resolves plugin references for a ScheduledPlugin instance. - * This must be done after deserialization since Plugin objects can't be - * serialized directly. - */ - private void resolvePluginReferences(PluginScheduleEntry scheduled) { - if (scheduled.getName() == null) { - return; - } - - // Find the plugin by name - Plugin plugin = Microbot.getPluginManager().getPlugins().stream() - .filter(p -> p.getName().equals(scheduled.getName())) - .findFirst() - .orElse(null); - - if (plugin != null) { - scheduled.setPlugin(plugin); - - // If plugin implements StoppingConditionProvider, make sure any plugin-defined - // conditions are properly registered - if (plugin instanceof SchedulablePlugin) { - log.debug("Found StoppingConditionProvider plugin: {}", plugin.getName()); - // This will preserve user-defined conditions while adding plugin-defined ones - //scheduled.registerPluginStoppingConditions(); - } - } else { - log.warn("Could not find plugin with name: {}", scheduled.getName()); - } - } - - public List getAvailablePlugins() { - return Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - return descriptor != null && plugin instanceof SchedulablePlugin; - - }) - .map(Plugin::getName) - .sorted() - .collect(Collectors.toList()); - } - - public PluginScheduleEntry getNextPluginToBeScheduled() { - return getNextScheduledPluginEntry(true, null).orElse(null); - } - - public PluginScheduleEntry getUpComingPluginWithinTime(Duration timeWindow) { - return getNextScheduledPluginEntry(false, timeWindow, false).orElse(null); - } - public PluginScheduleEntry getUpComingPlugin() { - return getNextScheduledPluginEntry(false, null, true).orElse(null); - } - - /** - * Backward compatibility method - delegates to the main method with excludeCurrentPlugin = false - */ - public Optional getNextScheduledPluginEntry(boolean isDueToRun, Duration timeWindow) { - return getNextScheduledPluginEntry(isDueToRun, timeWindow, false); - } - - /** - * Core method to find the next plugin based on various criteria. - * This uses sortPluginScheduleEntries with weighted selection to handle - * randomizable plugins. - * - * The selection priority depends on the timeWindow parameter: - * - * When timeWindow is NULL (immediate execution context): - * 1. Plugins that are due to run NOW get priority over priority level - * 2. Within due/not-due groups: later sort by scheduler group, earliest timing, over all groups, - * ->> find the group with the erlist timing(has a plugin which is upcomming next) - * 3. Sorted using the enhanced sortPluginScheduleEntries method - * - * When timeWindow is PROVIDED (looking ahead context): - * 1. Highest priority plugins get priority (regardless of due-to-run status) - * 2. Within sch groups: earliest timing and due-to-run status via sorting - * 3. Sorted using the enhanced sortPluginScheduleEntries method - * - * @param isDueToRun If true, only returns plugins that are due to run now - * @param timeWindow If not null, limits to plugins triggered within this time - * window and changes prioritization to favor priority over due-status - * @param excludeCurrentPlugin If true, excludes the currently running plugin from selection - * @return Optional containing the next plugin to run, or empty if none match - * criteria - */ - public Optional getNextScheduledPluginEntry(boolean isDueToRun, - Duration timeWindow, - boolean excludeCurrentPlugin) { - - if (scheduledPlugins.isEmpty()) { - return Optional.empty(); - } - // Apply filters based on parameters - List filteredPlugins = scheduledPlugins.stream() - .filter(PluginScheduleEntry::isEnabled) - .filter(PluginScheduleEntry::isPluginAvailable) // ensure only available plugins can be scheduled - .filter(plugin -> { - // Exclude currently running plugin if requested (for UI display purposes) - if (excludeCurrentPlugin && plugin.equals(currentPlugin) && plugin.isRunning()) { - log.debug("Excluding currently running plugin '{}' from upcoming selection", plugin.getCleanName()); - return false; - } - - // Filter by whether it's due to run now if requested - if (isDueToRun && !plugin.isDueToRun()) { - log.debug("Plugin '{}' is not due to run", plugin.getCleanName()); - return false; - } - if (plugin.isStopInitiated()) { - log.debug("Plugin '{}' has stop initiated", plugin.getCleanName()); - return false; - } - - // Filter by time window if specified - if (timeWindow != null) { - Optional nextStartTime = plugin.getCurrentStartTriggerTime(); - if (!nextStartTime.isPresent()) { - log.debug("Plugin '{}' has no trigger time", plugin.getCleanName()); - return false; - } - - ZonedDateTime cutoffTime = ZonedDateTime.now(ZoneId.systemDefault()).plus(timeWindow); - if (nextStartTime.get().isAfter(cutoffTime)) { - log.debug("Plugin '{}' trigger time is after cutoff", plugin.getCleanName()); - return false; - } - } - - // Must have a valid next trigger time - return plugin.getCurrentStartTriggerTime().isPresent(); - }) - .collect(Collectors.toList()); - - if (filteredPlugins.isEmpty()) { - return Optional.empty(); - } - // Different prioritization logic based on whether we're looking within a time window - List candidatePlugins; - if (timeWindow != null) { - - //TODO when we add scheduler groups, we need to filter by group name here - // When looking within a time window, we want to prioritize by priority first - // and then by earliest start time within that priority group - // This ensures we see the highest priority plugins that are coming up next - - // When looking within a time window, prioritize by priority first (user wants to see what's coming up) - // Find the highest priority plugins within the time window - int highestPriority = filteredPlugins.stream() - .mapToInt(PluginScheduleEntry::getPriority) - .max() - .orElse(0); - - candidatePlugins = filteredPlugins.stream() - .filter(p -> p.getPriority() == highestPriority) - .collect(Collectors.toList()); - - } else { - // When no time window, prioritize due-to-run status over priority (for immediate execution) - List duePlugins = filteredPlugins.stream() - .filter(PluginScheduleEntry::isDueToRun) - .collect(Collectors.toList()); - List notDuePlugins = filteredPlugins.stream() - .filter(p -> !p.isDueToRun()) - .collect(Collectors.toList()); - // Choose the appropriate group - prefer due plugins when available - List candidateGroup = !duePlugins.isEmpty() ? duePlugins : notDuePlugins; - - candidatePlugins = candidateGroup; - // NOTE: not filtering by priority here, later we want to implement, scheuler groups, but need to think about how to handle that - // what is a scheduler group? -> which attrribute we add to a PluginScheduleEntry? within a group, plugins can have different priorities - // i think scheduler groups, could be string identifiers, like "combat", "skilling", "questing" etc. - //candidatePlugins = candidateGroup.stream() - // .filter(p -> p.getScheulderGroup().toLowerCast().contains(groupName.toLowerCase())) - // .collect(Collectors.toList()); - } - // Sort the candidate plugins with weighted selection - // This handles both randomizable and non-randomizable plugins - List sortedCandidates = SchedulerPluginUtil.sortPluginScheduleEntries(candidatePlugins, true); - //log.debug("Sorted candidate plugins: {}", sortedCandidates.stream() - // .map((entry) -> {return "name: "+entry.getCleanName() + "next start: " + entry.getNextRunDisplay();}) - // .collect(Collectors.joining(", "))); - // The first plugin after sorting is our selected plugin - if (!sortedCandidates.isEmpty()) { - PluginScheduleEntry selectedPlugin = sortedCandidates.get(0); - return Optional.of(selectedPlugin); - } - - return Optional.empty(); - } - - /** - * Helper method to check if all plugins in a list have the same start trigger - * time - * (truncated to millisecond precision for stable comparisons) - * - * @param plugins List of plugins to check - * @return true if all plugins have the same trigger time - */ - private boolean isAllSameTimestamp(List plugins) { - return SchedulerPluginUtil.isAllSameTimestamp(plugins); - } - - /** - * Checks if the current plugin should be stopped based on conditions - */ - private void checkCurrentPlugin() { - if (currentPlugin == null || !currentPlugin.isRunning()) { - // should not happen because only called when is isScheduledPluginRunning() is - // true - return; - //throw new IllegalStateException("No current plugin is running"); - } - - // Monitor pre/post task state changes and update scheduler state accordingly - monitorPrePostTaskState(); - - // Call the update hook if the plugin is a condition provider - Plugin runningPlugin = currentPlugin.getPlugin(); - if (runningPlugin instanceof SchedulablePlugin) { - ((SchedulablePlugin) runningPlugin).onStopConditionCheck(); - } - - if(currentPlugin.isPaused() && isOnBreak()){ - return; - } - - // Log condition progress if debug mode is enabled - if (Microbot.isDebug()) { - // Log current progress of all conditions - currentPlugin.logConditionInfo(currentPlugin.getStopConditions(), "DEBUG_CHECK Running Plugin", true); - - // If there are progress-tracking conditions, log their progress percentage - double overallProgress = currentPlugin.getStopConditionProgress(); - if (overallProgress > 0) { - log.info("Overall condition progress for '{}': {}%", - currentPlugin.getCleanName(), - String.format("%.1f", overallProgress)); - } - } - - // Check if conditions are met - boolean stopStarted = currentPlugin.checkConditionsAndStop(true); - if (currentPlugin.isRunning() && !stopStarted - && currentState != SchedulerState.SOFT_STOPPING_PLUGIN - && currentState != SchedulerState.HARD_STOPPING_PLUGIN - && currentState != SchedulerState.EXECUTING_POST_SCHEDULE_TASKS - && currentPlugin.isDefault()){ - boolean prioritizeNonDefaultPlugins = config.prioritizeNonDefaultPlugins(); - // Use the configured look-ahead time window - int nonDefaultPluginLookAheadMinutes = config.nonDefaultPluginLookAheadMinutes(); - PluginScheduleEntry nextPluginWithin = getNextScheduledPluginEntry(true, - Duration.ofMinutes(nonDefaultPluginLookAheadMinutes)).orElse(null); - - if (nextPluginWithin != null && !nextPluginWithin.isDefault()) { - //String builder - StringBuilder sb = new StringBuilder(); - sb.append("\nPlugin '").append(currentPlugin.getCleanName()).append("' is running and has a next scheduled plugin within ") - .append(nonDefaultPluginLookAheadMinutes) - .append(" minutes that is not a default plugin: '") - .append(nextPluginWithin.getCleanName()).append("'"); - log.info(sb.toString()); - - } - - if(prioritizeNonDefaultPlugins && nextPluginWithin != null && !nextPluginWithin.isDefault()){ - log.info("Try to Stop default plugin '{}' because a non-default plugin '{}'' is scheduled to run within {} minutes", - currentPlugin.getCleanName(), nextPluginWithin.getCleanName(),nonDefaultPluginLookAheadMinutes); - currentPlugin.setLastStopReason("Plugin '" + nextPluginWithin.getCleanName() + "' is scheduled to run within " + nonDefaultPluginLookAheadMinutes + " minutes"); - stopStarted = currentPlugin.stop(true, - StopReason.INTERRUPTED, - "Plugin '" + nextPluginWithin.getCleanName() + "' is scheduled to run within " + nonDefaultPluginLookAheadMinutes + " minutes"); - - } - } - if (stopStarted) { - if (currentState != SchedulerState.EXECUTING_POST_SCHEDULE_TASKS) { - if (config.notificationsOn()){ - String notificationMessage = "SoftStop Plugin '" + currentPlugin.getCleanName() + "' stopped because conditions were met or non-default plugin is scheduled to run soon"; - notifier.notify(Notification.ON, notificationMessage); - } - if (hasDisabledQoLPlugin){ - // SchedulerPluginUtil.enablePlugin(QoLPlugin.class); - } - log.info("Plugin '{}' stopped because conditions were met", - currentPlugin.getCleanName()); - // Set state to indicate we're stopping the plugin - setState(SchedulerState.SOFT_STOPPING_PLUGIN); - } - } - if (!currentPlugin.isRunning()) { - log.info("Plugin '{}' stopped because conditions were met", - currentPlugin.getCleanName()); - setCurrentPlugin(null); - setState(SchedulerState.SCHEDULING); - } - } - - /** - * Gets condition progress for a scheduled plugin. - * - * @param scheduled The scheduled plugin - * @return Progress percentage (0-100) - */ - public double getStopConditionProgress(PluginScheduleEntry scheduled) { - if (scheduled == null || scheduled.getStopConditionManager().getConditions().isEmpty()) { - return 0; - } - - return scheduled.getStopConditionProgress(); - } - - /** - * Gets the list of plugins that have conditions set - */ - public List getPluginsWithConditions() { - return scheduledPlugins.stream() - .filter(p -> !p.getStopConditionManager().getConditions().isEmpty()) - .collect(Collectors.toList()); - } - - - - /** - * Adds conditions to a scheduled plugin with support for saving to a specific file - * - * @param plugin The plugin to add conditions to - * @param stopConditions List of stop conditions - * @param startConditions List of start conditions - * @param requireAll Whether all conditions must be met - * @param stopOnConditionsMet Whether to stop the plugin when conditions are met - * @param saveFile Optional file to save the conditions to, or null to use default config - */ - public void saveConditionsToPlugin(PluginScheduleEntry plugin, List stopConditions, - List startConditions, boolean requireAll, boolean stopOnConditionsMet, File saveFile) { - if (plugin == null) - return; - - // Clear existing stop conditions - plugin.getStopConditionManager().getConditions().clear(); - - // Add new stop conditions - for (Condition condition : stopConditions) { - plugin.addStopCondition(condition); - } - - // Add start conditions if provided - if (startConditions != null) { - plugin.getStartConditionManager().getConditions().clear(); - for (Condition condition : startConditions) { - plugin.addStartCondition(condition); - } - } - - // Set condition manager properties - if (requireAll) { - plugin.getStopConditionManager().setRequireAll(); - } else { - plugin.getStopConditionManager().setRequireAny(); - } - - // Save to specified file if provided, otherwise to config - if (saveFile != null) { - savePluginScheduleEntriesToFile(saveFile); - } else { - // Save to config - saveScheduledPlugins(); - } - } - - - - /** - * Checks if a specific plugin schedule entry is currently running - * This explicitly compares by reference, not just by name - */ - public boolean isRunningEntry(PluginScheduleEntry entry) { - return entry.isRunning(); - } - - /** - * Checks if a completed one-time plugin should be removed - * - * @param scheduled The scheduled plugin to check - * @return True if the plugin should be removed - */ - private boolean shouldRemoveCompletedOneTimePlugin(PluginScheduleEntry scheduled) { - // Check if it's been run at least once and is not currently running - boolean hasRun = scheduled.getRunCount() > 0 && !scheduled.isRunning(); - - // Check if it can't be triggered again (based on start conditions) - boolean cantTriggerAgain = !scheduled.canStartTriggerAgain(); - - return hasRun && cantTriggerAgain; - } - - /** - * Cleans up the scheduled plugins list by removing completed one-time plugins - */ - private void cleanupCompletedOneTimePlugins() { - List toRemove = scheduledPlugins.stream() - .filter(this::shouldRemoveCompletedOneTimePlugin) - .collect(Collectors.toList()); - - if (!toRemove.isEmpty()) { - scheduledPlugins.removeAll(toRemove); - saveScheduledPlugins(); - log.info("Removed {} completed one-time plugins", toRemove.size()); - } - } - - public boolean isScheduledPluginRunning() { - return currentPlugin != null && currentPlugin.isRunning(); - } - - private boolean isBreakHandlerEnabled() { - return SchedulerPluginUtil.isBreakHandlerEnabled(); - } - - /** - * checks if the game state is fully loaded after login - * ensures all game systems are ready before starting plugins - */ - private boolean isGameStateFullyLoaded() { - if (!Microbot.isLoggedIn()) { - return false; - } - - // check that client and game state are properly initialized - if (Microbot.getClient() == null) { - return false; - } - - GameState gameState = Microbot.getClient().getGameState(); - if (gameState != GameState.LOGGED_IN) { - return false; - } - @Component - final int WELCOME_SCREEN_COMPONENT_ID = 24772680; - if (Rs2Widget.isWidgetVisible(WELCOME_SCREEN_COMPONENT_ID)) { - log.debug("Welcome screen is active, waiting for it to close"); - return false; - } - - // check that player data is loaded - if (Rs2Player.getWorldLocation() == null) { - log.debug("Player world location not yet available, waiting for full game state load"); - return false; - } - - - - // check that cache system is fully loaded and player profile is ready - if (!Rs2CacheManager.isCacheDataValid()) { - log.debug("Cache system not yet fully loaded, waiting for profile initialization"); - return false; - } - - return true; - } - - private boolean isAntibanEnabled() { - return SchedulerPluginUtil.isAntibanEnabled(); - } - - private boolean isAutoLoginEnabled() { - return SchedulerPluginUtil.isAutoLoginEnabled(); - } - - /** - * Forces the bot to take a break immediately if BreakHandler is enabled - * - * @return true if break was initiated, false otherwise - */ - private boolean forceBreak() { - return SchedulerPluginUtil.forceBreak(); - } - - private boolean takeMicroBreak() { - return SchedulerPluginUtil.takeMicroBreak(() -> setState(SchedulerState.BREAK)); - } - - private boolean lockBreakHandler() { - return SchedulerPluginUtil.lockBreakHandler(); - } - - private void unlockBreakHandler() { - SchedulerPluginUtil.unlockBreakHandler(); - } - - /** - * Checks if the bot is currently on a break - * - * @return true if on break, false otherwise - */ - public boolean isOnBreak() { - return SchedulerPluginUtil.isOnBreak(); - } - - /** - * Gets the current activity being performed - * - * @return The current activity, or null if not tracking - */ - public Activity getCurrentActivity() { - return currentActivity; - } - - /** - * Gets the current activity intensity - * - * @return The current activity intensity, or null if not tracking - */ - public ActivityIntensity getCurrentIntensity() { - return currentIntensity; - } - - /** - * Gets the current idle time in game ticks - * - * @return Idle time (ticks) - */ - public int getIdleTime() { - return idleTime; - } - - /** - * Gets the time elapsed since login - * - * @return Duration since login, or Duration.ZERO if not logged in - */ - public Duration getLoginDuration() { - if (loginTime == null) { - return Duration.ZERO; - } - return Duration.between(loginTime, Instant.now()); - } - - /** - * Gets the time since last detected activity - * - * @return Duration since last activity - */ - public Duration getTimeSinceLastActivity() { - return Duration.between(lastActivityTime, Instant.now()); - } - - /** - * Gets the time until the next scheduled break - * - * @return Duration until next break - */ - public Duration getTimeUntilNextBreak() { - if (SchedulerPluginUtil.isBreakHandlerEnabled() && BreakHandlerScript.breakIn >= 0) { - return Duration.ofSeconds(BreakHandlerScript.breakIn); - } - return timeUntilNextBreak; - } - - /** - * Gets the duration of the current break - * - * @return Current break duration - */ - public Duration getCurrentBreakDuration() { - if (SchedulerPluginUtil.isBreakHandlerEnabled() && BreakHandlerScript.breakDuration > 0) { - return Duration.ofSeconds(BreakHandlerScript.breakDuration); - } - return currentBreakDuration; - } - /** - * resets manual login state and restores automatic break handling - */ - public void resetManualLoginState() { - if (getCurrentState() == SchedulerState.MANUAL_LOGIN_ACTIVE) { - log.info("resetting manual login state - restoring automatic break handling"); - - // unlock break handler - if (SchedulerPluginUtil.isBreakHandlerEnabled()) { - BreakHandlerScript.setLockState(false); - log.debug("unlocked break handler for automatic break management"); - } - - // reset plugin pause event state - PluginPauseEvent.setPaused(false); - - // return to appropriate state (usually scheduling) - setState(SchedulerState.SCHEDULING); - } - } - - /** - * handles manual login/logout toggle with proper state management - */ - public void toggleManualLogin() { - SchedulerState currentState = getCurrentState(); - boolean isLoggedIn = Microbot.isLoggedIn(); - - // only allow manual login/logout in appropriate states - boolean isInManualLoginState = currentState == SchedulerState.MANUAL_LOGIN_ACTIVE; - boolean isInSchedulingState = currentState == SchedulerState.SCHEDULING || - currentState == SchedulerState.WAITING_FOR_SCHEDULE; - boolean canUseManualLogin = isInManualLoginState || isInSchedulingState || - currentState == SchedulerState.BREAK || - currentState == SchedulerState.PLAYSCHEDULE_BREAK; - - if (!canUseManualLogin) { - log.warn("manual login/logout not allowed in current state: {}", currentState); - return; - } - - if (isInManualLoginState) { - // user wants to logout and return to automatic break handling - log.info("manual logout requested - resuming automatic break handling"); - - // unlock break handler - BreakHandlerScript.setLockState(false); - log.info("unlocked break handler for automatic break management"); - - // reset plugin pause event state - PluginPauseEvent.setPaused(false); - SchedulerPluginUtil.disableAllRunningNonEessentialPlugin(); - // logout if logged in but -> the "SCHEDULING" state should determine if we need to logout. - // return to scheduling state - setState(SchedulerState.SCHEDULING); - log.info("returned to automatic scheduling mode"); - - } else { - // user wants to login manually and pause automatic breaks - log.info("manual login requested - pausing automatic break handling"); - - // interrupt current break if active - boolean shouldCancelBreak = isOnBreak() && - (currentState == SchedulerState.BREAK || currentState == SchedulerState.PLAYSCHEDULE_BREAK); - - if (shouldCancelBreak) { - log.info("interrupting active break for manual login"); - interruptBreak(); - } - - // set manual login active state - setState(SchedulerState.MANUAL_LOGIN_ACTIVE); - - // lock break handler to prevent automatic breaks - BreakHandlerScript.setLockState(true); - log.info("locked break handler to prevent automatic breaks"); - // pause plugin execution during manual control - PluginPauseEvent.setPaused(true); - - // start login process if not already logged in - if (!isLoggedIn) { - startLoginMonitoringThread(shouldCancelBreak); - } else { - log.info("already logged in - manual break pause mode activated"); - } - } - } - - public void startLoginMonitoringThread(boolean cancelBreak) { - if (cancelBreak && isOnBreak()) { - log.info("Cancelling break before starting login monitoring thread"); - interruptBreak(); - } - startLoginMonitoringThread(); - } - - public void startLoginMonitoringThread() { - String pluginName = ""; - if (!currentState.isSchedulerActive() - || (currentState.isBreaking() - || currentState == SchedulerState.LOGIN ) - || isOnBreak() - ||(Microbot.isHopping() - || Microbot.isLoggedIn())) { - log.info("Login monitoring thread not started\n" - + " -current state: {} \n" - + " -is waiting: {} \n" - + " -is breaking: {} \n" - + " -is paused: {} \n" - + " -is scheduler active: {} \n" - + " -is on break: {} \n" - + " -is break handler enabled: {} \n" - + " -logged in: {} \n" - + " -hopping: {}", - currentState, - currentState.isWaiting(), - currentState.isBreaking(), - currentState.isPaused(), - currentState.isSchedulerActive(), - isOnBreak(), - isBreakHandlerEnabled(), - Microbot.isLoggedIn(), - Microbot.isHopping()); - return; - } - if (currentPlugin != null) { - pluginName = currentPlugin.getName(); - } - - if (loginMonitor != null && loginMonitor.isAlive()) { - log.info("Login monitoring thread already running for plugin '{}'", pluginName); - return; - } - setState(SchedulerState.LOGIN); - this.loginMonitor = new Thread(() -> { - try { - - log.debug("Login monitoring thread started for plugin"); - int loginAttempts = 0; - final int MAX_LOGIN_ATTEMPTS = 6; - - // Keep checking until login completes or max attempts reached - while (loginAttempts < MAX_LOGIN_ATTEMPTS) { - // Wait for login attempt to complete - - log.info("Login attempt {} of {}", - loginAttempts, MAX_LOGIN_ATTEMPTS); - // Try login again if needed - - if (loginAttempts < MAX_LOGIN_ATTEMPTS) { - login(); - } - if (Microbot.isLoggedIn()) { - // Successfully logged in, now increment the run count - if (currentPlugin != null) { - log.info("Login successful, finalizing plugin start: {}", currentPlugin.getName()); - if (currentPlugin.isRunning()) { - // If we were running the plugin, continue with that - log.info("Continuing to run plugin after login: {}", currentPlugin.getName()); - setState(SchedulerState.RUNNING_PLUGIN); - - - - }else if(!currentPlugin.isRunning()){ - // If we were starting the plugin, continue with that - setState(SchedulerState.STARTING_PLUGIN); - log.info("Continuing to start plugin after login: {}", currentPlugin.getName()); - Microbot.getClientThread().invokeLater(() -> { - monitorStartingPluginScheduleEntry(currentPlugin); - // setState(SchedulerState.RUNNING_PLUGIN); - }); - - } - - return; - }else{ - log.info("Login successful, but no plugin to start back to scheduling"); - setState(SchedulerState.SCHEDULING); - } - return; - } - if (Microbot.getClient().getGameState() != GameState.LOGGED_IN && - Microbot.getClient().getGameState() != GameState.LOGGING_IN) { - loginAttempts++; - } - Thread.sleep(2000); - } - - // If we get here, login failed too many times - log.error("Failed to login after {} attempts", - MAX_LOGIN_ATTEMPTS); - SwingUtilities.invokeLater(() -> { - // Clean up and set proper state - if (currentPlugin != null && currentPlugin.isRunning()) { - currentPlugin.stop(false, StopReason.SCHEDULED_STOP, "Plugin stopped due to scheduled time conditions"); - setState(SchedulerState.SOFT_STOPPING_PLUGIN); - } else { - if (currentPlugin != null) { - currentPlugin.setEnabled(false); - } - log.error("Failed to login, stopping plugin: {}", currentPlugin != null ? currentPlugin.getName() : "none"); - currentPlugin = null; - setState(SchedulerState.SCHEDULING); - } - - }); - } catch (InterruptedException e) { - if (currentPlugin != null) log.debug("Login monitoring thread for '{}' was interrupted", currentPlugin.getName()); - } - }); - - loginMonitor.setName("LoginMonitor - " + pluginName); - loginMonitor.setDaemon(true); - loginMonitor.start(); - } - - private void logout() { - SchedulerPluginUtil.logout(); - } - - private void login() { - // First check if AutoLogin plugin is available and enabled - if (!isAutoLoginEnabled() && config.autoLogIn()) { - // Try to enable AutoLogin plugin - if (SchedulerPluginUtil.enableAutoLogin()) { - // Give it a moment to initialize - try { - Thread.sleep(500); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - } - - // Fallback to manual login if AutoLogin is not available - boolean successfulLogin = Microbot.getClientThread().runOnClientThreadOptional(() -> { - if (Microbot.getClient() == null || Microbot.getClient().getGameState() != GameState.LOGIN_SCREEN) { - log.warn("Cannot login, client is not in login screen state"); - return false; - } - // check which login index means we are in authifcation or not a member - // TODO add these to "LOGIN" class -> - // net.runelite.client.plugins.microbot.util.security - int currentLoginIndex = Microbot.getClient().getLoginIndex(); - boolean tryMemberWorld = config.worldType() == 2 || config.worldType() == 1 ; // TODO get correct one - ConfigProfile profile = LoginManager.getActiveProfile(); - tryMemberWorld = profile != null && profile.isMember(); - if (currentLoginIndex == 4 || currentLoginIndex == 3) { // we are in the auth screen and cannot login - // 3 mean wrong authtifaction - return false; // we are in auth - } - if (currentLoginIndex == 34) { // we are not a member and cannot login - if (isAutoLoginEnabled() || config.autoLogInWorld() == 1) { - Microbot.getConfigManager().setConfiguration("AutoLoginConfig", "World", - LoginManager.getRandomWorld(false)); - } - int loginScreenWidth = 804; - int startingWidth = (Microbot.getClient().getCanvasWidth() / 2) - (loginScreenWidth / 2); - Microbot.getMouse().click(365 + startingWidth, 308); // clicks a button "OK" when you've been - // disconnected - sleep(600); - if (config.worldType() != 2){ - // Show dialog for free world selection using the SchedulerUIUtils class - SchedulerUIUtils.showNonMemberWorldDialog(currentPlugin, config, (switchToFreeWorlds) -> { - if (!switchToFreeWorlds) { - // User chose not to switch to free worlds or dialog timed out - if (currentPlugin != null) { - currentPlugin.setEnabled(false); - currentPlugin = null; - setState(SchedulerState.SCHEDULING); - log.info("Login to member world canceled, stopping current plugin"); - } - } - }); - } - tryMemberWorld = false; // we are not a member - - - } - if (currentLoginIndex == 2) { - // connected to the server - } - - if (isAutoLoginEnabled() ) { - if (Microbot.pauseAllScripts.get()) { - log.info("AutoLogin is enabled but paused, stopping AutoLogin"); - - } - ConfigManager configManager = Microbot.getConfigManager(); - if (configManager != null) { - configManager.setConfiguration("AutoLoginConfig", "World", - LoginManager.getRandomWorld(tryMemberWorld)); - } - // Give it a moment to initialize - try { - Thread.sleep(500); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } else { - int worldID = LoginManager.getRandomWorld(tryMemberWorld); - log.info("\n\tforced login by scheduler plugin \n\t-> currentLoginIndex: {} - member World {}? - world {}", currentLoginIndex, - tryMemberWorld, worldID); - LoginManager.login(worldID); - } - return true; - }).orElse(false); - - } - - /** - * Prints detailed diagnostic information about all scheduled plugins - */ - public void debugAllScheduledPlugins() { - log.info("==== PLUGIN SCHEDULER DIAGNOSTICS ===="); - log.info("Current state: {}", currentState); - log.info("Number of scheduled plugins: {}", scheduledPlugins.size()); - - for (PluginScheduleEntry plugin : scheduledPlugins) { - log.info("\n----- Plugin: {} -----", plugin.getCleanName()); - log.info("Enabled: {}", plugin.isEnabled()); - log.info("Running: {}", plugin.isRunning()); - log.info("Is default: {}", plugin.isDefault()); - log.info("Due to run: {}", plugin.isDueToRun()); - log.info("Has start conditions: {}", plugin.hasAnyStartConditions()); - - if (plugin.hasAnyStartConditions()) { - log.info("Start conditions met: {}", plugin.getStartConditionManager().areAllConditionsMet()); - - // Get next trigger time if any - Optional nextTrigger = plugin.getCurrentStartTriggerTime(); - log.info("Next trigger time: {}", - nextTrigger.isPresent() ? nextTrigger.get() : "None found"); - - // Print detailed diagnostics - log.info("\nDetailed start condition diagnosis:"); - log.info(plugin.diagnoseStartConditions()); - } - } - log.info("==== END DIAGNOSTICS ===="); - } - - - - - - public void openAntibanSettings() { - // Get the parent frame - SwingUtilities.invokeLater(() -> { - try { - // Use the utility class to open the Antiban settings in a new window - AntibanDialogWindow.showAntibanSettings(panel); - } catch (Exception ex) { - log.error("Error opening Antiban settings: {}", ex.getMessage()); - } - }); - } - - /** - * Sets the current scheduler state and updates UI - */ - private void setState(SchedulerState newState) { - if (currentState != newState) { - prvState = currentState; - log.debug("Scheduler state changed: {} -> {}", currentState, newState); - breakStartTime = Optional.empty(); - // Set additional state information based on context - switch (newState) { - case INITIALIZING: - newState.setStateInformation(String.format("Checking for required plugins (%d/%d)", - initCheckCount, MAX_INIT_CHECKS)); - break; - - case ERROR: - newState.setStateInformation(String.format( - "Initialization failed after %d/%d attempts. Client may not be at login screen.", - initCheckCount, MAX_INIT_CHECKS)); - break; - - case WAITING_FOR_LOGIN: - newState.setStateInformation("Waiting for player to login to start plugin"); - break; - - case SOFT_STOPPING_PLUGIN: - newState.setStateInformation( - currentPlugin != null ? "Attempting to gracefully stop " + currentPlugin.getCleanName() - : "Attempting to gracefully stop plugin"); - break; - - case HARD_STOPPING_PLUGIN: - newState.setStateInformation( - currentPlugin != null ? "Forcing " + currentPlugin.getCleanName() + " to stop" - : "Forcing plugin to stop"); - break; - - case RUNNING_PLUGIN: - if (currentPlugin != null && currentPlugin.isPaused()) { - currentPlugin.resume(); // Resume if paused - break; - } - newState.setStateInformation( - currentPlugin != null ? "Running " + currentPlugin.getCleanName() : "Running plugin"); - break; - - case STARTING_PLUGIN: - newState.setStateInformation( - currentPlugin != null ? "Starting " + currentPlugin.getCleanName() : "Starting plugin"); - break; - - case EXECUTING_PRE_SCHEDULE_TASKS: - String preTaskInfo = getPrePostTaskStateInfo(); - PluginScheduleEntry currentRunningPlugin = getCurrentPlugin(); - if ( currentRunningPlugin != null && !currentRunningPlugin.isPaused() && currentRunningPlugin.isRunning()) { - currentRunningPlugin.pause(); // pause plaugin so its not processing the stop conditions while we are doing pre-schedule tasks - } - newState.setStateInformation( - currentPlugin != null ? - "Executing pre-schedule tasks for " + currentPlugin.getCleanName() + - (preTaskInfo != null ? "\n" + preTaskInfo : "") : - "Executing pre-schedule tasks"); - break; - - case EXECUTING_POST_SCHEDULE_TASKS: - String postTaskInfo = getPrePostTaskStateInfo(); - newState.setStateInformation( - currentPlugin != null ? - "Executing post-schedule tasks for " + currentPlugin.getCleanName() + - (postTaskInfo != null ? "\n" + postTaskInfo : "") : - "Executing post-schedule tasks"); - break; - - case BREAK: - PluginScheduleEntry nextPlugin = getUpComingPlugin(); - if (nextPlugin != null) { - Optional nextStartTime = nextPlugin.getCurrentStartTriggerTime(); - String displayTime = nextStartTime.isPresent() ? - nextStartTime.get().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z")) : - "unknown time"; - newState.setStateInformation("Taking break until " + - nextPlugin.getCleanName() + " is scheduled to run at " + displayTime - ); - } else { - newState.setStateInformation("Taking a break between schedules"); - } - breakStartTime = Optional.of(ZonedDateTime.now(ZoneId.systemDefault())); - break; - case PLAYSCHEDULE_BREAK: - - Duration timeUntilNext = config.playSchedule().timeUntilNextSchedule(); - //LocalTime startTime = config.playSchedule().getStartTime(); - //LocalTime endTime = config.playSchedule().getEndTime(); - if (timeUntilNext != null) { - newState.setStateInformation("Taking a break Play Schedule: \n\t" + config.playSchedule().displayString()); - } else { - newState.setStateInformation("Taking a break between schedules"); - } - breakStartTime = Optional.of(ZonedDateTime.now(ZoneId.systemDefault())); - //breakEndTime = Optional.of(ZonedDateTime.now(ZoneId.systemDefault()).plus(timeUntilNext)); - break; - case WAITING_FOR_SCHEDULE: - newState.setStateInformation("Waiting for the next scheduled plugin to become due"); - break; - - case SCHEDULING: - newState.setStateInformation("Actively checking plugin schedules"); - break; - - case READY: - newState.setStateInformation("Ready to run - click Start to begin scheduling"); - break; - - case HOLD: - newState.setStateInformation("Scheduler was manually stopped"); - break; - - case SCHEDULER_PAUSED: - newState.setStateInformation("Scheduler is paused. Resume to continue."); - break; - case RUNNING_PLUGIN_PAUSED: - newState.setStateInformation("Running plugin is paused. Resume to continue."); - break; - default: - newState.setStateInformation(""); // Clear any previous information - break; - } - - currentState = newState; - SwingUtilities.invokeLater(this::forceUpdatePanels); - } - } - - /** - * Checks if the player has been idle for longer than the specified timeout - * - * @param timeout The timeout in game ticks - * @return True if idle for longer than timeout - */ - public boolean isIdleTooLong(int timeout) { - return idleTime > timeout && !isOnBreak(); - } - - - @Subscribe(priority = 100) - private void onClientShutdown(ClientShutdown e) { - if (currentPlugin != null && currentPlugin.isRunning()) { - log.info("Client shutdown detected, stopping current plugin: {}", currentPlugin.getCleanName()); - // Stop the current plugin gracefully - currentPlugin.stop(false, StopReason.CLIENT_SHUTDOWN, "Client is shutting down"); - setState(SchedulerState.SCHEDULING); - } - } - @Subscribe - public void onGameTick(GameTick event) { - // Update idle time tracking - if (!Rs2Player.isAnimating() && !Rs2Player.isMoving() && !isOnBreak() - && this.currentState == SchedulerState.RUNNING_PLUGIN) { - idleTime++; - } else { - idleTime = 0; - } - } - - @Subscribe - public void onPluginScheduleEntryMainTaskFinishedEvent(PluginScheduleEntryMainTaskFinishedEvent event) { - if (currentPlugin != null && event.getPlugin() == currentPlugin.getPlugin()) { - log.info("Plugin '{}' self-reported as finished: {} (Result: {})", - currentPlugin.getCleanName(), - event.getReason(), - event.getResult().getDisplayName()); - - // Handle soft failure tracking - ExecutionResult result = event.getResult(); - if (result == ExecutionResult.SUCCESS) { - currentPlugin.recordSuccess(); - } else if (result == ExecutionResult.SOFT_FAILURE) { - currentPlugin.recordSoftFailure(event.getReason()); - } - // Hard failures don't need special tracking since they immediately disable the plugin - - if (config.notificationsOn()){ - String notificationMessage = "Plugin '" + currentPlugin.getCleanName() + "' finished: " + event.getReason(); - if (result.isSuccess()) { - notificationMessage += " (Success)"; - } else if (result.isSoftFailure()) { - notificationMessage += " (Soft Failure - Can Retry)"; - } else { - notificationMessage += " (Hard Failure)"; - } - notifier.notify(Notification.ON, notificationMessage); - - } - - // Stop the plugin with the success state from the event - if (currentState == SchedulerState.RUNNING_PLUGIN || currentState == SchedulerState.RUNNING_PLUGIN_PAUSED) { - setState(SchedulerState.SOFT_STOPPING_PLUGIN); - } - - // Format the reason message for better readability - String eventReason = event.getReason(); - String formattedReason = SchedulerPluginUtil.formatReasonMessage(eventReason); - - String reasonMessage = event.isSuccess() ? - "Plugin completed its task successfully:\n\t\t\"" + formattedReason+"\"": - "Plugin reported completion but indicated an unsuccessful run:\n" + formattedReason; - - currentPlugin.stop(event.isSuccess(), StopReason.PLUGIN_FINISHED, reasonMessage); - - } - } - - @Subscribe - public void onPluginScheduleEntryPreScheduleTaskFinishedEvent(PluginScheduleEntryPreScheduleTaskFinishedEvent event) { - if (currentPlugin != null && event.getPlugin() == currentPlugin.getPlugin()) { - log.info("Plugin '{}' startup completed: {} (Success: {})", - currentPlugin.getCleanName(), - event.getMessage(), - event.isSuccess()); - - if (event.isSuccess()) { - if (currentState == SchedulerState.STARTING_PLUGIN || currentState == SchedulerState.EXECUTING_PRE_SCHEDULE_TASKS) { - // Plugin startup successful - transition to running state - setState(SchedulerState.RUNNING_PLUGIN); - log.info("Plugin '{}' started successfully and is now running", currentPlugin.getCleanName()); - } - } else { - // Plugin startup failed - stop the plugin and return to scheduling - log.error("\n\tPlugin '{}' startup failed: {}", currentPlugin.getCleanName(), event.getMessage()); - if(currentPlugin.isPaused()){ - currentPlugin.resume(); // Resume if paused - } - if (config.notificationsOn()) { - notifier.notify(Notification.ON, - "Plugin '" + currentPlugin.getCleanName() + "' startup failed: " + event.getMessage()); - } - - // Clean up and return to scheduling - //currentPlugin = null; - currentPlugin.stop(event.isSuccess(), StopReason.PREPOST_SCHEDULE_STOP, "Startup failed: " + event.getMessage() ); - setState(SchedulerState.HARD_STOPPING_PLUGIN); - - } - - } - } - - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskFinishedEvent(PluginScheduleEntryPostScheduleTaskFinishedEvent event) { - if (currentPlugin != null && event.getPlugin() == currentPlugin.getPlugin()) { - ExecutionResult result = event.getResult(); - log.info("Plugin '{}' post-schedule tasks completed: {} (Result: {})", - currentPlugin.getCleanName(), - event.getMessage(), - result.getDisplayName()); - - // Handle soft failure tracking - if (result == ExecutionResult.SUCCESS) { - currentPlugin.recordSuccess(); - log.info("Plugin '{}' post-schedule tasks completed successfully", currentPlugin.getCleanName()); - } else if (result == ExecutionResult.SOFT_FAILURE) { - currentPlugin.recordSoftFailure(event.getMessage()); - log.warn("Plugin '{}' post-schedule tasks soft failure: {} (Consecutive soft failures: {})", - currentPlugin.getCleanName(), event.getMessage(), currentPlugin.getConsecutiveSoftFailures()); - - if (config.notificationsOn()) { - notifier.notify(Notification.ON, - "Plugin '" + currentPlugin.getCleanName() + "' post-schedule soft failure: " + event.getMessage()); - } - } else { // HARD_FAILURE - log.warn("Plugin '{}' post-schedule tasks failed: {}", currentPlugin.getCleanName(), event.getMessage()); - - if (config.notificationsOn()) { - notifier.notify(Notification.ON, - "Plugin '" + currentPlugin.getCleanName() + "' post-schedule cleanup failed: " + event.getMessage()); - } - } - - - - - // Format the reason message for better readability - String eventReason = event.getMessage() + (currentPlugin.getLastStopReasonType() == PluginScheduleEntry.StopReason.NONE ? "": currentPlugin.getLastStopReason()); - String formattedReason = SchedulerPluginUtil.formatReasonMessage(eventReason); - - String reasonMessage = result.isSuccess() ? - "Plugin completed its post task successfully:\n\t\t\t\"" + formattedReason+"\"": - "Plugin completed its post task not:\n\t\t\t" + formattedReason; - // Regardless of success/failure, post-schedule tasks are done - // The state transition will be handled by monitorPrePostTaskState when it detects IDLE phase - if(currentPlugin.isRunning()){ - if (currentPlugin.isStopping()){ - currentPlugin.cancelStop(); - } - // Stop the plugin with the success state from the event - if ( currentState == SchedulerState.EXECUTING_POST_SCHEDULE_TASKS - || currentState == SchedulerState.SOFT_STOPPING_PLUGIN - ) { - currentPlugin.stop(result, StopReason.PREPOST_SCHEDULE_STOP, reasonMessage ); - setState(SchedulerState.HARD_STOPPING_PLUGIN); - }else if( currentState == SchedulerState.RUNNING_PLUGIN || currentState == SchedulerState.RUNNING_PLUGIN_PAUSED){ - // If we were executing pre-schedule tasks, just stop the plugin - currentPlugin.stop(result, StopReason.PLUGIN_FINISHED, reasonMessage); - setState(SchedulerState.SOFT_STOPPING_PLUGIN); - } - - } - } - } - - - @Subscribe - public void onGameStateChanged(GameStateChanged gameStateChanged) { - - // Track login time - if (gameStateChanged.getGameState() == GameState.LOGGED_IN - && (lastGameState == GameState.LOGIN_SCREEN || lastGameState == GameState.HOPPING)) { - loginTime = Instant.now(); - // Reset idle counter on login - idleTime = 0; - } - - if (gameStateChanged.getGameState() == GameState.LOGGED_IN) { - // If the game state is LOGGED_IN, start the scheduler - - } else if (gameStateChanged.getGameState() == GameState.LOGIN_SCREEN) { - // If the game state is LOGIN_SCREEN, stop the current plugin - - // Clear login time when logging out - loginTime = null; - } else if (gameStateChanged.getGameState() == GameState.HOPPING) { - // If the game state is HOPPING, stop the current plugin - - } else if (gameStateChanged.getGameState() == GameState.CONNECTION_LOST) { - // If the game state is CONNECTION_LOST, stop the current plugin - // Clear login time when connection is lost - loginTime = null; - - } else if (gameStateChanged.getGameState() == GameState.LOGIN_SCREEN_AUTHENTICATOR) { - // If the game state is LOGGING_IN, stop the current plugin - - // Clear login time when logging out - loginTime = null; - stopScheduler(); - - } - - this.lastGameState = gameStateChanged.getGameState(); - } - - @Subscribe - public void onConfigChanged(ConfigChanged event) { - if (event.getGroup().equals("PluginScheduler")) { - // Handle overlay toggle - if (event.getKey().equals("showOverlay")) { - if (config.showOverlay()) { - overlayManager.add(overlay); - } else { - overlayManager.remove(overlay); - } - } - - // Update plugin configurations - for (PluginScheduleEntry plugin : scheduledPlugins) { - plugin.setSoftStopRetryInterval(Duration.ofSeconds(config.softStopRetrySeconds())); - plugin.setHardStopTimeout(Duration.ofSeconds(config.hardStopTimeoutSeconds())); - } - } - } - - @Subscribe - public void onStatChanged(StatChanged statChanged) { - // Reset idle time when gaining experience - idleTime = 0; - lastActivityTime = Instant.now(); - - final Skill skill = statChanged.getSkill(); - final int exp = statChanged.getXp(); - final Integer previous = skillExp.put(skill, exp); - - if (lastSkillChanged != null && (lastSkillChanged.equals(skill) || - (CombatSkills.isCombatSkill(lastSkillChanged) && CombatSkills.isCombatSkill(skill)))) { - - return; - } - - lastSkillChanged = skill; - - if (previous == null || previous >= exp) { - return; - } - - // Update our local tracking of activity - Activity activity = Activity.fromSkill(skill); - if (activity != null) { - currentActivity = activity; - if (Microbot.isDebug()) { - log.debug("Activity updated from skill: {} -> {}", skill.getName(), activity); - } - } - - ActivityIntensity intensity = ActivityIntensity.fromSkill(skill); - if (intensity != null) { - currentIntensity = intensity; - } - } - - @Subscribe - public void onPluginChanged(PluginChanged event) { - if (currentPlugin != null && event.getPlugin() == currentPlugin.getPlugin()) { - // The plugin changed state - check if it's no longer running - boolean isRunningNow = currentPlugin.isRunning(); - boolean wasStartedByScheduler = currentPlugin.isHasStarted(); - - // If plugin was running but is now stopped - if (!isRunningNow) { - log.info("\n\tPlugin '{}' state change detected: \n\t -from running to stopped", currentPlugin.getCleanName()); - - // Check if this was an expected stop based on our current state - boolean wasExpectedStop = (currentState == SchedulerState.SOFT_STOPPING_PLUGIN || - currentState == SchedulerState.HARD_STOPPING_PLUGIN); - - // If the stop wasn't initiated by us, it was unexpected (error or manual stop) - if (!wasExpectedStop && currentState == SchedulerState.RUNNING_PLUGIN) { - log.warn("Plugin '{}' stopped unexpectedly while in {} state", - currentPlugin.getCleanName(), currentState.name()); - - // Set error information - currentPlugin.setLastStopReason("Plugin stopped unexpectedly"); - currentPlugin.setLastRunSuccessful(false); - currentPlugin.setLastStopReasonType(PluginScheduleEntry.StopReason.ERROR); - // Disable the plugin to prevent it from running again until issue is fixed - currentPlugin.setEnabled(false); - currentPlugin.setHasStarted(false); - // Set state to error - - } else if (currentState == SchedulerState.SOFT_STOPPING_PLUGIN|| currentState == SchedulerState.EXECUTING_POST_SCHEDULE_TASKS) { - // If we were soft stopping and it completed, make sure stop reason is set - // Set stop reason if it wasn't already set - if (currentPlugin.getLastStopReasonType() == PluginScheduleEntry.StopReason.NONE) { - currentPlugin.setLastStopReasonType(PluginScheduleEntry.StopReason.SCHEDULED_STOP); - currentPlugin.setLastStopReason("Scheduled stop completed successfully"); - currentPlugin.setLastRunSuccessful(true); - currentPlugin.setHasStarted(false); - } - - - - } else if (currentState == SchedulerState.HARD_STOPPING_PLUGIN) { - // Hard stop completed - if (currentPlugin.getLastStopReasonType() == PluginScheduleEntry.StopReason.NONE) { - currentPlugin.setLastStopReasonType(PluginScheduleEntry.StopReason.HARD_STOP); - currentPlugin.setLastStopReason("Plugin was forcibly stopped after timeout"); - currentPlugin.setLastRunSuccessful(false); - currentPlugin.setHasStarted(false); - } - - } - - // Return to scheduling state regardless of stop reason - // BUT only after post-schedule tasks are complete (if any) - if (currentState != SchedulerState.HOLD) { - // Check if we need to wait for post-schedule tasks - if (currentState == SchedulerState.EXECUTING_POST_SCHEDULE_TASKS) { - log.error("Plugin '{}' stopped but post-schedule tasks are still running - the post schedule task has not report finished reported finished ?", - currentPlugin.getCleanName()); - // should not happen - } - - log.info("\nPlugin '{}' stopped \n\t- returning to scheduling state with reason: \n\t\t\"{}\"", - currentPlugin.getCleanName(), - currentPlugin.getLastStopReason()); - - setState(SchedulerState.SCHEDULING); - currentPlugin.cancelStop(); - setCurrentPlugin(null); - - } else { - // In HOLD state, still clear the current plugin - currentPlugin.cancelStop(); - currentPlugin.setHasStarted(false); - setCurrentPlugin(null); - } - // Microbot.getClientThread().invokeLater(() -> { - // Check if the plugin is still stopping - // checkIfStopFinished(); - //}); - - - } else if (isRunningNow && wasStartedByScheduler && currentState == SchedulerState.SCHEDULING) { - // Plugin was started by scheduler and is now running - this is expected - log.info("Plugin '{}' started by scheduler and is now running", event.getPlugin().getName()); - - } else if (isRunningNow && wasStartedByScheduler && currentState != SchedulerState.STARTING_PLUGIN) { - // Plugin was started outside our control or restarted - this is unexpected but - // we'll monitor it - log.info("Plugin '{}' started or restarted outside scheduler control", event.getPlugin().getName()); - } - - SwingUtilities.invokeLater(this::updatePanels); - } - } - void checkIfStopFinished(){ - - log.info("current state after plugin stop: {}", currentState); - sleepUntilOnClientThread(() -> { return !currentPlugin.isStopping();}, 1000); - // Check if the plugin is still stopping - if (currentPlugin.isStopping()) { - log.info("Plugin '{}' is still stopping, waiting for it to finish", currentPlugin.getCleanName()); - SwingUtilities.invokeLater(() -> { - // Check if the plugin is still stopping - checkIfStopFinished(); - }); - } else { - log.info("Plugin '{}' is not stopping, continuing", currentPlugin.getCleanName()); - } - saveScheduledPlugins();//start conditions could have changed -> next trigger,..., save transient data - - - // Clear current plugin reference - setCurrentPlugin(null); - } - - /** - * Sorts all scheduled plugins according to a consistent order. - * criteria. - * - * @param applyWeightedSelection Whether to apply weighted selection for - * randomizable plugins - * @return A sorted list of all scheduled plugins - */ - public List sortPluginScheduleEntries(boolean applyWeightedSelection) { - return SchedulerPluginUtil.sortPluginScheduleEntries(scheduledPlugins, applyWeightedSelection); - } - /** - * Overloaded method that calls sortPluginScheduleEntries without weighted - * selection by default - */ - public List sortPluginScheduleEntries() { - return SchedulerPluginUtil.sortPluginScheduleEntries(scheduledPlugins, false); - } - /** - * Extends an active break when it's about to end and there are no upcoming plugins - * @param thresholdSeconds Time in seconds before break end when we consider extending - * @return true if break was extended, false otherwise - */ - private boolean extendBreakIfNeeded(PluginScheduleEntry nextPlugin, int thresholdSeconds) { - // Check if we're on a break and have break information - if (!isOnBreak() || !breakStartTime.isPresent() || currentBreakDuration.equals(Duration.ZERO) - || !currentState.isBreaking()) { - if (!isOnBreak() &¤tState.isBreaking()){ - interruptBreak(); - } - - return false; - } - if (isOnBreak()){ - this.currentBreakDuration = Duration.ofSeconds(BreakHandlerScript.breakDuration); - } - // Calculate when the current break will end - ZonedDateTime breakEndTime = breakStartTime.get().plus(this.currentBreakDuration); - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - - // Calculate how much time is left in the current break - Duration timeRemaining = Duration.between(now, breakEndTime); - // If the break is about to end within the threshold seconds - if (timeRemaining.getSeconds() <= thresholdSeconds) { - if (nextPlugin == null) { - // No upcoming plugin, extend the break - log.info("Break is about to end in {} seconds with no upcoming plugins. Extending break.", - timeRemaining.getSeconds()); - int minDuration = config.minBreakDuration(); - int maxDuration = config.maxBreakDuration(); - // Ensure min and max durations are valid - - - startBreakBetweenSchedules(config.autoLogOutOnBreak(), minDuration, maxDuration); - this.breakStartTime = Optional.of(now); - log.info("Break extended, no upcomming plugin detected New end time: {}", now.plus(this.currentBreakDuration)); - return true; - } - } - - return false; - } - - /** - * Manually start a plugin from the UI. This method ensures that: - * 1. The scheduler is in a safe state (SCHEDULING or SHORT_BREAK) - * 2. The requested plugin is in the scheduledPlugins list - * 3. There's enough time until the next scheduled plugin - * - * @param pluginEntry The plugin to start - * @return true if the plugin was started successfully, false otherwise with a reason message - */ - public String manualStartPlugin(PluginScheduleEntry pluginEntry) { - // Check if plugin is null - if (pluginEntry == null) { - return "Invalid plugin selected"; - } - if (pluginEntry.getMainTimeStartCondition()!=null && pluginEntry.getMainTimeStartCondition().canTriggerAgain()){ - TimeCondition mainTimeStartCondition = pluginEntry.getMainTimeStartCondition(); - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - boolean isSatisfied = mainTimeStartCondition.isSatisfiedAt(now); - if (!isSatisfied) { - //return "Cannot start plugin: Main time condition is not satisfied"; - log.warn("\n\tMain time condition is not satisfied, setting next trigger time to now \n\t{}",mainTimeStartCondition.toString() - ); - } - mainTimeStartCondition.setNextTriggerTime(now); - } - // Check if scheduler is in a safe state to start a plugin - if (currentState != SchedulerState.SCHEDULING && !currentState.isBreaking() - && currentState != SchedulerState.WAITING_FOR_SCHEDULE) { - return "Cannot start plugin in current state: \n\t" + currentState.getDisplayName(); - } - if(currentState == SchedulerState.SCHEDULER_PAUSED || currentState == SchedulerState.RUNNING_PLUGIN_PAUSED){ - return "Cannot start plugin: \n\tScheduler is paused"; - } - - // Check if a plugin is already running - if (isScheduledPluginRunning()) { - return "Cannot start plugin: \n\tAnother plugin is already running"; - } - - // Check if the plugin is in the scheduled plugins list - if (!scheduledPlugins.contains(pluginEntry)) { - return "Cannot start plugin: \n\tPlugin is not in the scheduled plugins list"; - } - - // Check if the plugin is enabled - if (!pluginEntry.isEnabled()) { - return "Cannot start plugin: \n\tPlugin is disabled"; - } - - // Check time until next scheduled plugin - PluginScheduleEntry nextUpComingPlugin = getUpComingPlugin(); - if (nextUpComingPlugin != null && !nextUpComingPlugin.equals(pluginEntry)) { - Optional nextStartTime = nextUpComingPlugin.getCurrentStartTriggerTime(); - if (nextStartTime.isPresent()) { - Duration timeUntilNext = Duration.between( - ZonedDateTime.now(ZoneId.systemDefault()), nextStartTime.get()); - - int minThreshold = config.minManualStartThresholdMinutes(); - - if (timeUntilNext.toMinutes() < minThreshold) { - return "Cannot start plugin: \n\tNext scheduled plugin due in less than " + - minThreshold + " minute(s)"; - } - } - } - - // If we're on a break, interrupt it - if (currentState.isBreaking()) { - log.info("\n--Interrupting break to manually start plugin: \n\t\n--\"{}\"", pluginEntry.getCleanName()); - interruptBreak(); - } - - // Start the plugin - log.info("Manually starting plugin: {}", pluginEntry.getCleanName()); - startPluginScheduleEntry(pluginEntry); - - return ""; // Empty string means success - } - - /** - * Register a stop completion callback with the given plugin schedule entry. - * The callback will save the scheduled plugins state when a plugin stop is completed. - * - * @param entry The plugin schedule entry to register the callback with - */ - private void registerStopCompletionCallback(PluginScheduleEntry entry) { - entry.setStopCompletionCallback((stopEntry, wasSuccessful) -> { - // Save scheduled plugins state when a plugin stop is completed - saveScheduledPlugins(); - log.info("\n\t -Saved scheduled plugins after stop completion for plugin \n\t\t'{}'", - stopEntry.getName()); - }); - } - - - - /** - * Checks if the scheduler or the currently running plugin is paused. - * - * @return true if either the scheduler or the current plugin is paused - */ - public boolean isPaused() { - return currentState == SchedulerState.SCHEDULER_PAUSED || - currentState == SchedulerState.RUNNING_PLUGIN_PAUSED; - } - - public boolean isCurrentPluginPaused() { - if (getCurrentPlugin() == null) { - return false; // No current plugin - } - return getCurrentPlugin().isPaused() && - (currentState.isPaused() || currentState.isBreaking()); - } - public boolean allPluginEntryPaused() { - // Check if all scheduled plugins are paused - return scheduledPlugins.stream().allMatch(PluginScheduleEntry::isPaused); - } - public boolean anyPluginEntryPaused() { - // Check if any scheduled plugin is paused - return scheduledPlugins.stream().anyMatch(PluginScheduleEntry::isPaused); - } - private void pauseAllScheduledPlugins() { - scheduledPlugins.stream().map( PluginScheduleEntry::pause); - - } - private void resumeAllScheduledPlugins() { - scheduledPlugins.stream().map( PluginScheduleEntry::resume); - } - public boolean pauseRunningPlugin(){ - if (currentState != SchedulerState.RUNNING_PLUGIN || getCurrentPlugin() == null) { - return false; // Not running a plugin - } - if (currentState != SchedulerState.RUNNING_PLUGIN){ - log.error("Scheduler state is not RUNNING_PLUGIN, but {}", currentState); - return false; // Not paused - } - // Use the PluginPauseEvent to pause the current plugin - PluginPauseEvent.setPaused(true); - - - - setState(SchedulerState.RUNNING_PLUGIN_PAUSED); - - // Also pause time conditions on the current plugin - getCurrentPlugin().pause(); - log.info("Paused currently running plugin: {}", getCurrentPlugin().getName()); - SwingUtilities.invokeLater(this::forceUpdatePanels); - return true; - } - - public boolean resumeRunningPlugin(){ - if(isOnBreak() ){ - log.info("Interrupting break to resume running plugin: {}", getCurrentPlugin().getName()); - interruptBreak(); - - } - if ( ( currentState != SchedulerState.RUNNING_PLUGIN_PAUSED && !currentState.isBreaking())|| getCurrentPlugin() == null) { - log.error("resumeRunningPlugin - Scheduler state is", currentState); - return false; // Not paused - } - if (prvState != SchedulerState.RUNNING_PLUGIN ){ - log.error("Prv Scheduler state is not RUNNING_PLUGIN_PAUSED, but {}", prvState); - return false; // Not paused - } - if (isCurrentPluginPaused() == false) { - log.error("Current plugin is not paused, but {}", currentState); - return false; // Not paused - } - - // Restore previous state - setState(SchedulerState.RUNNING_PLUGIN); - - // Use the PluginPauseEvent to resume the current plugin - PluginPauseEvent.setPaused(false); - - // resume time conditions on the current plugin - getCurrentPlugin().resume(); - - - - boolean anyPausedPluginEntry = anyPluginEntryPaused(); - log.info("resumed currently running plugin: {} -> are any paused plugin? -{} - Pause Event? -{}", getCurrentPlugin().getName(),anyPausedPluginEntry, - PluginPauseEvent.isPaused()); - SwingUtilities.invokeLater(this::forceUpdatePanels); - return true; - } - - /** - * Pauses the scheduler or the currently running plugin. - * If a plugin is currently running, it will be paused using the PluginPauseEvent. - * Otherwise, the entire scheduler will be paused. - * - * @return true if successfully paused, false otherwise - */ - public boolean pauseScheduler() { - if (isPaused()) { - return false; // Already paused - } - if (getCurrentPlugin() != null && currentState == SchedulerState.RUNNING_PLUGIN) { - // Use the PluginPauseEvent to pause the current plugin - PluginPauseEvent.setPaused(true); - } - - setState(SchedulerState.SCHEDULER_PAUSED); - - - // Pause time conditions for all scheduled plugins - for (PluginScheduleEntry entry : scheduledPlugins) { - entry.pause(); - } - - SwingUtilities.invokeLater(this::forceUpdatePanels); - return true; - } - - /** - * resumes the scheduler or the currently running plugin. - * - * @return true if successfully resumed, false otherwise - */ - public boolean resumeScheduler() { - if (!isPaused()) { - return false; // Not paused - } - SchedulerState prvStateLocal = this.prvState; - if (getCurrentPlugin() != null && prvStateLocal == SchedulerState.RUNNING_PLUGIN) { - // Use the PluginPauseEvent to pause the current plugin - PluginPauseEvent.setPaused(false); - } - - if(isOnBreak() && prvStateLocal == SchedulerState.RUNNING_PLUGIN && currentState.isBreaking() ){ - interruptBreak(); - setState( SchedulerState.RUNNING_PLUGIN); - log.info("resuming the plugin scheduler and interrupted break"); - }else if (currentState == SchedulerState.SCHEDULER_PAUSED || currentState.isBreaking()) { - // Restore previous state - if (currentState.isBreaking() && !isOnBreak()){ - if(currentPlugin!=null ){ - setState( SchedulerState.RUNNING_PLUGIN); - currentPlugin.resume(); - log.info("resumed scheduler in to running plugin, previous state: {}", prvStateLocal); - }else{ - setState(SchedulerState.SCHEDULING); - log.info("resumed scheduler in to waiting for schedule, previous state: {}", prvStateLocal); - } - }else if (isOnBreak()){ - setState(SchedulerState.BREAK); - log.info("resumed scheduler in to break, previous state: {}", prvStateLocal); - }else{ - setState(prvStateLocal); - } - - }else{ - log.error("Cannot resume scheduler, current state is: {}", currentState); - return false; // Not paused - } - // resume time conditions for all scheduled plugins - for (PluginScheduleEntry entry : scheduledPlugins) { - entry.resume(); - } - boolean anyPausedPluginEntry = anyPluginEntryPaused(); - if (getCurrentPlugin() != null) { - getCurrentPlugin().resume(); - log.info("resumed the scheduler plugin: {} -> are any paused plugin? -{} - Pause Event? -{}", getCurrentPlugin().getName(),anyPausedPluginEntry, - PluginPauseEvent.isPaused()); - } - - SwingUtilities.invokeLater(this::forceUpdatePanels); - return true; - } - - - /** - * Gets the estimated time until the next scheduled plugin will be ready to run. - * This method uses the new estimation system to provide more accurate predictions - * for when plugins will be scheduled, considering both current running plugins - * and upcoming plugin start conditions. - * - * @return Optional containing the estimated duration until the next plugin can be scheduled - */ - public Optional getUpComingEstimatedScheduleTime() { - // First, check if we have a currently running plugin that might stop soon - Optional currentPluginStopEstimate = getCurrentPluginEstimatedStopTime(); - - // Get the next upcoming plugin - PluginScheduleEntry upcomingPlugin = getUpComingPlugin(); - if (upcomingPlugin == null) { - // If no upcoming plugin, return the current plugin's estimated stop time - return currentPluginStopEstimate; - } - - // Get the estimated start time for the upcoming plugin - Optional upcomingPluginStartEstimate = upcomingPlugin.getEstimatedStartTimeWhenIsSatisfied(); - - // If we have both estimates, return the longer one (more conservative estimate) - if (currentPluginStopEstimate.isPresent() && upcomingPluginStartEstimate.isPresent()) { - Duration stopTime = currentPluginStopEstimate.get(); - Duration startTime = upcomingPluginStartEstimate.get(); - return Optional.of(stopTime.compareTo(startTime) > 0 ? stopTime : startTime); - } - - // Return whichever estimate we have - return upcomingPluginStartEstimate.isPresent() ? upcomingPluginStartEstimate : currentPluginStopEstimate; - } - - /** - * Gets the estimated time until the next plugin will be scheduled within a specific time window. - * This method considers both current plugin stop conditions and upcoming plugin start conditions - * within the specified time frame. - * - * @param timeWindow The time window to look ahead for upcoming plugins - * @return Optional containing the estimated duration until the next plugin can be scheduled within the window - */ - public Optional getUpComingEstimatedScheduleTimeWithinTime(Duration timeWindow) { - // First, check if we have a currently running plugin that might stop soon - Optional currentPluginStopEstimate = getCurrentPluginEstimatedStopTime(); - - // Get the next upcoming plugin within the time window - PluginScheduleEntry upcomingPlugin = getUpComingPluginWithinTime(timeWindow); - if (upcomingPlugin == null) { - // If no upcoming plugin within time window, return current plugin's stop estimate - // but only if it's within the time window - if (currentPluginStopEstimate.isPresent() && - currentPluginStopEstimate.get().compareTo(timeWindow) <= 0) { - return currentPluginStopEstimate; - } - return Optional.empty(); - } - - // Get the estimated start time for the upcoming plugin - Optional upcomingPluginStartEstimate = upcomingPlugin.getEstimatedStartTimeWhenIsSatisfied(); - - // Filter estimates to only include those within the time window - if (upcomingPluginStartEstimate.isPresent() && - upcomingPluginStartEstimate.get().compareTo(timeWindow) > 0) { - upcomingPluginStartEstimate = Optional.empty(); - } - - if (currentPluginStopEstimate.isPresent() && - currentPluginStopEstimate.get().compareTo(timeWindow) > 0) { - currentPluginStopEstimate = Optional.empty(); - } - - // If we have both estimates within the window, return the longer one - if (currentPluginStopEstimate.isPresent() && upcomingPluginStartEstimate.isPresent()) { - Duration stopTime = currentPluginStopEstimate.get(); - Duration startTime = upcomingPluginStartEstimate.get(); - return Optional.of(stopTime.compareTo(startTime) > 0 ? stopTime : startTime); - } - - // Return whichever estimate we have within the window - return upcomingPluginStartEstimate.isPresent() ? upcomingPluginStartEstimate : currentPluginStopEstimate; - } - - /** - * Gets the estimated time until the currently running plugin will stop. - * This considers user-defined stop conditions for the current plugin. - * - * @return Optional containing the estimated duration until the current plugin stops - */ - private Optional getCurrentPluginEstimatedStopTime() { - if (currentPlugin == null || !currentPlugin.isRunning()) { - return Optional.empty(); - } - - return currentPlugin.getEstimatedStopTimeWhenIsSatisfied(); - } - - /** - * Gets a formatted string representation of the estimated schedule time. - * - * @return A human-readable string describing when the next plugin is estimated to be scheduled - */ - public String getUpComingEstimatedScheduleTimeDisplay() { - Optional estimate = getUpComingEstimatedScheduleTime(); - if (estimate.isPresent()) { - return formatEstimatedScheduleDuration(estimate.get()); - } - return "Cannot estimate next schedule time"; - } - - /** - * Gets a formatted string representation of the estimated schedule time within a time window. - * - * @param timeWindow The time window to consider - * @return A human-readable string describing when the next plugin is estimated to be scheduled - */ - public String getUpComingEstimatedScheduleTimeWithinTimeDisplay(Duration timeWindow) { - Optional estimate = getUpComingEstimatedScheduleTimeWithinTime(timeWindow); - if (estimate.isPresent()) { - return formatEstimatedScheduleDuration(estimate.get()); - } - return "No plugins estimated within time window"; - } - - /** - * Helper method to format estimated schedule durations into human-readable strings. - * - * @param duration The duration to format - * @return A formatted string representation - */ - private String formatEstimatedScheduleDuration(Duration duration) { - long seconds = duration.getSeconds(); - - if (seconds <= 0) { - return "Next plugin can be scheduled now"; - } else if (seconds < 60) { - return String.format("Next plugin estimated in ~%d seconds", seconds); - } else if (seconds < 3600) { - return String.format("Next plugin estimated in ~%d minutes", seconds / 60); - } else if (seconds < 86400) { - return String.format("Next plugin estimated in ~%d hours", seconds / 3600); - } else { - long days = seconds / 86400; - return String.format("Next plugin estimated in ~%d days", days); - } - } - - /** - * Gets the pre/post schedule tasks for the current plugin if available. - * - * @return The AbstractPrePostScheduleTasks instance, or null if current plugin doesn't have tasks - */ - public AbstractPrePostScheduleTasks getCurrentPluginPrePostTasks() { - if (currentPlugin == null ) { - return null; - } - return currentPlugin.getPrePostTasks(); - } - - /** - * Gets the task execution state for the current plugin's pre/post schedule tasks. - * - * @return The TaskExecutionState, or null if no tasks are available - */ - public TaskExecutionState getCurrentPluginTaskExecutionState() { - AbstractPrePostScheduleTasks tasks = getCurrentPluginPrePostTasks(); - return tasks != null ? tasks.getExecutionState() : null; - } - - /** - * Checks if the current plugin has pre/post schedule tasks configured. - * - * @return true if the current plugin has pre/post schedule tasks, false otherwise - */ - public boolean currentPluginHasPrePostTasks() { - return getCurrentPluginPrePostTasks() != null; - } - - /** - * Checks if the current plugin's pre/post schedule tasks are currently executing. - * - * @return true if tasks are executing, false otherwise - */ - public boolean currentPluginTasksAreExecuting() { - TaskExecutionState state = getCurrentPluginTaskExecutionState(); - return state != null && state.isExecuting(); - } - - /** - * Gets the current execution phase of the current plugin's tasks. - * - * @return The ExecutionPhase, or IDLE if no tasks are executing - */ - public ExecutionPhase getCurrentPluginTaskPhase() { - TaskExecutionState state = getCurrentPluginTaskExecutionState(); - return state != null ? state.getCurrentPhase() : ExecutionPhase.IDLE; - } - - /** - * Gets detailed information about the current pre/post task execution state. - * - * @return A formatted string with current task state information, or null if no tasks are executing - */ - private String getPrePostTaskStateInfo() { - TaskExecutionState state = getCurrentPluginTaskExecutionState(); - if (state == null || state.getCurrentPhase() == ExecutionPhase.IDLE) { - return null; - } - - StringBuilder info = new StringBuilder(); - info.append("State: ").append(state.getCurrentState().getDisplayName()); - - if (state.getCurrentDetails() != null && !state.getCurrentDetails().isEmpty()) { - info.append("\nDetails: ").append(state.getCurrentDetails()); - } - - if (state.getTotalSteps() > 0) { - info.append("\nProgress: ").append(state.getCurrentStepNumber()).append("/").append(state.getTotalSteps()).append(" steps"); - } - - if (state.getCurrentRequirementName() != null && !state.getCurrentRequirementName().isEmpty()) { - info.append("\nCurrent: ").append(state.getCurrentRequirementName()); - } - - info.append("\nExecution phase completion:"); - // Add integer values for phase completion tracking - info.append("\n hasOreTaskStarted: ").append(state.isHasPreTaskStarted() ? 1 : 0); - info.append(" hasPreTaskCompleted: ").append(state.isHasPreTaskCompleted() ? 1 : 0); - info.append(" hasMainTaskStarted: ").append(state.isHasMainTaskStarted() ? 1 : 0); - info.append(" hasMainTaskCompleted: ").append(state.isHasMainTaskCompleted() ? 1 : 0); - info.append(" hasPostTaskStarted: ").append(state.isHasPostTaskStarted() ? 1 : 0); - info.append(" hasPostTaskCompleted: ").append(state.isHasPostTaskCompleted() ? 1 : 0); - - return info.toString(); - } - - /** - * Previous task execution phase for state change detection - */ - private TaskExecutionState.ExecutionPhase previousTaskPhase = TaskExecutionState.ExecutionPhase.IDLE; - - /** - * Monitors pre/post task state changes and updates scheduler state accordingly. - * This method efficiently detects state transitions rather than continuously checking. - */ - private void monitorPrePostTaskState() { - AbstractPrePostScheduleTasks currentPluginPrePostScheduleTask = getCurrentPluginPrePostTasks(); - TaskExecutionState currentTaskState = getCurrentPluginTaskExecutionState(); - TaskExecutionState.ExecutionPhase currentTaskExecutionPhase = getCurrentPluginTaskPhase(); - if (currentPluginPrePostScheduleTask == null) { - return; - } - if (currentState == SchedulerState.RUNNING_PLUGIN || currentState == SchedulerState.EXECUTING_PRE_SCHEDULE_TASKS) { - if( currentTaskState.canExecutePreTasks() && currentTaskExecutionPhase == TaskExecutionState.ExecutionPhase.IDLE){ - - if (currentPlugin != null && currentPlugin.isRunning() && currentPluginPrePostScheduleTask.canStartPreScheduleTasks()) { - // Check if plugin implements SchedulablePlugin and trigger pre-schedule tasks - Plugin plugin = currentPlugin.getPlugin(); - boolean triggered = currentPlugin.triggerPreScheduleTasks(); - log.debug(" -currentTaskExecutionPhase: {}, -isPreScheduleTasksStarted: {},\n\t -isPreScheduleTasksCompleted: {},\n\t isMainTaskStarted {} -isMainTaskCompleted {} \n\t-triggered: {}", - currentTaskExecutionPhase, currentTaskState.isHasPreTaskStarted(), currentTaskState.isHasPreTaskCompleted(), currentTaskState.isMainTaskRunning(),currentTaskState.isHasMainTaskCompleted(), triggered); - log.warn("Current phase is idle and we are in RUNNING_PLUGIN state, but pre-schedule tasks can be started: {}", - triggered); - } - } - } - if (currentState == SchedulerState.SOFT_STOPPING_PLUGIN || currentState == SchedulerState.EXECUTING_POST_SCHEDULE_TASKS) { - if( currentTaskState.canExecutePostTasks() && currentTaskExecutionPhase == TaskExecutionState.ExecutionPhase.MAIN_EXECUTION){ - - if (currentPlugin != null && currentPlugin.isRunning() && currentPluginPrePostScheduleTask.canStartPostScheduleTasks()) { - // Check if plugin implements SchedulablePlugin and trigger pre-schedule tasks - Plugin plugin = currentPlugin.getPlugin(); - //boolean triggered = currentPlugin.triggerPostScheduleTasks(hasDisabledQoLPlugin) (); - log.warn("Current phase is MAIN_EXECUTION and we are in SOFT_STOPPING_PLUGIN state, but post-schedule tasks can be started: {}", - currentPluginPrePostScheduleTask.canStartPostScheduleTasks()); - - } - } - } - // Only update state if there's been a phase change - if (currentTaskExecutionPhase != previousTaskPhase) { - log.debug("\ncurrent task state {}\n\tCurrent task phase: {} -> previous Phase {}: Current plugin task state: {}",currentTaskState, currentTaskExecutionPhase, previousTaskPhase, getPrePostTaskStateInfo()); - switch (currentTaskExecutionPhase) { - case PRE_SCHEDULE: - setState(SchedulerState.EXECUTING_PRE_SCHEDULE_TASKS); - break; - case POST_SCHEDULE: - setState(SchedulerState.EXECUTING_POST_SCHEDULE_TASKS); - break; - case MAIN_EXECUTION: - // Plugin is running but not in pre/post tasks - if (currentState == SchedulerState.EXECUTING_PRE_SCHEDULE_TASKS || - currentState == SchedulerState.EXECUTING_POST_SCHEDULE_TASKS) { - setState(SchedulerState.RUNNING_PLUGIN); - } - break; - case IDLE: - // Tasks have finished - return to appropriate state - if (currentState == SchedulerState.EXECUTING_PRE_SCHEDULE_TASKS) { - setState(SchedulerState.RUNNING_PLUGIN); - } else if (currentState == SchedulerState.EXECUTING_POST_SCHEDULE_TASKS) { - // Post-schedule tasks completed - return to scheduling - log.debug("Post-schedule tasks completed for plugin '{}' - returning to scheduling state", - currentPlugin != null ? currentPlugin.getCleanName() : "unknown"); - setState(SchedulerState.SOFT_STOPPING_PLUGIN); - } - break; - } - - this.previousTaskPhase = currentTaskExecutionPhase; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerState.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerState.java deleted file mode 100644 index 8d3f26502aa..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerState.java +++ /dev/null @@ -1,115 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler; - -import java.awt.Color; - -import lombok.Getter; -import lombok.Setter; - -/** - * Represents the various states the scheduler can be in - */ -public enum SchedulerState { - UNINITIALIZED("Uninitialized", "Plugin is not yet initialized", new Color(150, 150, 150)), - INITIALIZING("Initializing", "Plugin is initializing", new Color(255, 165, 0)), - READY("Ready", "Ready to run scheduled plugins", new Color(0, 150, 255)), - SCHEDULING("SCHEDULING", "Scheduler is running and monitoring", new Color(76, 175, 80)), - STARTING_PLUGIN("Starting Plugin", "Starting a scheduled plugin", new Color(200, 230, 0)), - EXECUTING_PRE_SCHEDULE_TASKS("Pre-Schedule Tasks", "Executing pre-schedule preparation tasks", new Color(173, 216, 230)), - RUNNING_PLUGIN("Running Plugin", "Scheduled plugin is running", new Color(0, 200, 83)), - RUNNING_PLUGIN_PAUSED("Plugin Paused", "Current plugin execution is paused", new Color(255, 140, 0)), - EXECUTING_POST_SCHEDULE_TASKS("Post-Schedule Tasks", "Executing post-schedule cleanup tasks", new Color(255, 182, 193)), - SCHEDULER_PAUSED("Scheduler Paused", "All scheduler activities are paused", new Color(255, 165, 0)), - WAITING_FOR_LOGIN("Waiting for Login", "Waiting for user to log in", new Color(255, 215, 0)), - HARD_STOPPING_PLUGIN("Hard Stopping Plugin", "Stopping the current plugin", new Color(255, 120, 0)), - SOFT_STOPPING_PLUGIN("Soft Stopping Plugin", "Stopping the current plugin", new Color(255, 120, 0)), - HOLD("Stopped", "Scheduler was manually stopped", new Color(244, 67, 54)), - ERROR("Error", "Scheduler encountered an error", new Color(255, 0, 0)), - BREAK("Break", "Taking a break until next plugin", new Color(100, 149, 237)), - PLAYSCHEDULE_BREAK("Play Schedule Break", "Breaking based on the configured Play Schedule", new Color(100, 149, 237)), - WAITING_FOR_SCHEDULE("Next Schedule Soon", "Waiting for upcoming scheduled plugin", new Color(147, 112, 219)), - WAITING_FOR_STOP_CONDITION("Waiting For Stop Condition", "Waiting For Stop Condition", new Color(255, 140, 0)), - LOGIN("Login", "Try To Login", new Color(255, 215, 0)), - MANUAL_LOGIN_ACTIVE("Manual Login Active", "User manually logged in - breaks paused", new Color(32, 178, 170)); - - private final String displayName; - private final String description; - private final Color color; - @Setter - @Getter - private String stateInformation = ""; - - SchedulerState(String displayName, String description, Color color) { - this.displayName = displayName; - this.description = description; - this.color = color; - } - - public String getDisplayName() { - return displayName; - } - - public String getDescription() { - return description; - } - - public Color getColor() { - return color; - } - public boolean isSchedulerActive() { - return this != SchedulerState.UNINITIALIZED && - this != SchedulerState.INITIALIZING && - this != SchedulerState.ERROR && - this != SchedulerState.HOLD && - this != SchedulerState.READY && !isPaused(); - } - - /** - * Determines if the scheduler is actively running a plugin or about to run one. - * This includes pre/post schedule task execution as part of plugin running. - */ - public boolean isPluginRunning() { - return isSchedulerActive() && - (this == SchedulerState.RUNNING_PLUGIN || - this == SchedulerState.EXECUTING_PRE_SCHEDULE_TASKS || - this == SchedulerState.EXECUTING_POST_SCHEDULE_TASKS); - } - - /** - * Determines if the scheduler is executing pre/post schedule tasks - */ - public boolean isExecutingPrePostTasks() { - return this == SchedulerState.EXECUTING_PRE_SCHEDULE_TASKS || - this == SchedulerState.EXECUTING_POST_SCHEDULE_TASKS; - } - public boolean isAboutStarting() { - return this == SchedulerState.STARTING_PLUGIN || this== SchedulerState.WAITING_FOR_STOP_CONDITION || - this == SchedulerState.WAITING_FOR_LOGIN; - } - - /** - * Determines if the scheduler is in a waiting state between scheduling a plugin - */ - public boolean isWaiting() { - return isSchedulerActive() && - (this == SchedulerState.SCHEDULING || - this == SchedulerState.WAITING_FOR_SCHEDULE || - this == SchedulerState.BREAK || this == SchedulerState.PLAYSCHEDULE_BREAK || - this == SchedulerState.MANUAL_LOGIN_ACTIVE); - } - public boolean isBreaking() { - return (this == SchedulerState.BREAK || this == SchedulerState.PLAYSCHEDULE_BREAK); - } - - public boolean isInitializing() { - return this == SchedulerState.INITIALIZING || this == SchedulerState.UNINITIALIZED; - } - public boolean isStopping() { - return this == SchedulerState.SOFT_STOPPING_PLUGIN || - this == SchedulerState.HARD_STOPPING_PLUGIN || this == SchedulerState.EXECUTING_POST_SCHEDULE_TASKS; - } - public boolean isPaused() { - return this == SchedulerState.SCHEDULER_PAUSED || - this == SchedulerState.RUNNING_PLUGIN_PAUSED; - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java deleted file mode 100644 index e0bfdbc985a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java +++ /dev/null @@ -1,687 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.api; - - -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.AndCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LockCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.event.ExecutionResult; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryMainTaskFinishedEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPostScheduleTaskEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPostScheduleTaskFinishedEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPreScheduleTaskEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPreScheduleTaskFinishedEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.AbstractPrePostScheduleTasks; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.util.SchedulerPluginUtil; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.event.Level; - -import com.google.common.eventbus.Subscribe; -import net.runelite.client.config.ConfigDescriptor; -import net.runelite.client.plugins.Plugin; -import net.runelite.client.plugins.microbot.Microbot; - -import java.awt.Component; -import java.awt.Window; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import javax.annotation.Nullable; -import javax.swing.*; - - - -/** - * Interface for plugins that want to provide custom stopping conditions and scheduling capabilities. - * Implement this interface in your plugin to define when the scheduler should stop it or - * to have your plugin report when it has finished its tasks. - */ - - -public interface SchedulablePlugin { - - Logger log = LoggerFactory.getLogger(SchedulablePlugin.class); - - - default LogicalCondition getStartCondition() { - return new AndCondition(); - } - /** - * Returns a logical condition structure that defines when the plugin should stop. - * - * This allows for complex logical combinations (AND, OR, NOT) of conditions. - * If this method returns null, the conditions from {@link #getStopConditions()} - * will be combined with AND logic (all conditions must be met). - *
- * Example for creating a complex condition: "(A OR B) AND C": - *
- * @Override - * public LogicalCondition getLogicalConditionStructure() { - * AndCondition root = new AndCondition(); - * OrCondition orGroup = new OrCondition(); - * - * orGroup.addCondition(conditionA); - * orGroup.addCondition(conditionB); - * - * root.addCondition(orGroup); - * root.addCondition(conditionC); - * - * return root; - * } - *- * - * @return A logical condition structure, or null to use simple AND logic - */ - default LogicalCondition getStopCondition(){ - return new AndCondition(); - } - - - /** - * Called periodically when conditions are being evaluated by the scheduler. - *- * Use this method to update any dynamic condition state if needed. This hook - * allows plugins to refresh condition values or state before they're evaluated. - * This method is called approximately once per second while the plugin is running. - */ - default void onStopConditionCheck() { - // Optional hook for condition updates - } - /** - * Handles the {@link PluginScheduleEntry} posted by the scheduler when stop conditions are met. - *
- * This event handler is automatically called when the scheduler determines that - * all required conditions for stopping have been met. The default implementation - * calls {@link Microbot#stopPlugin(net.runelite.client.plugins.Plugin)} to gracefully - * stop the plugin. - *
- * Plugin developers should generally not override this method unless they need - * custom stop behavior. If overridden, make sure to either call the default - * implementation or properly stop the plugin. - *
- * Note: This is an EventBus subscriber method and requires the implementing - * plugin to be registered with the EventBus for it to be called. - * - * @param event The stop event containing the plugin reference that should be stopped - */ - @Subscribe - default public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event){ - if (event.getPlugin() == this) { - log.info("Plugin must implement it onPluginScheduleEntryPostScheduleTaskEvent method to handle post-schedule tasks."); - } - } - /* - * Use {@link onPluginScheduleEntryPostScheduleTaskEvent} instead. - */ - @Deprecated - @Subscribe - default public void onPluginScheduleEntrySoftStopEvent(PluginScheduleEntryPostScheduleTaskEvent event){ - log.warn("onPluginScheduleEntrySoftStopEvent is deprecated. Use onPluginScheduleEntryPostScheduleTaskEvent instead."); - if (event.getPlugin() == this) { - log.info("Plugin must implement it onPluginScheduleEntryPostScheduleTaskEvent method to handle post-schedule tasks."); - } - } - - /** - * Handles the {@link PluginScheduleEntryPreScheduleTaskEvent} posted by the scheduler when a plugin should start pre-schedule tasks. - *
- * This event handler is called when the scheduler wants to trigger pre-schedule tasks for a plugin. - * The default implementation will run pre-schedule tasks (if available) and then execute the provided script callback. - *
- * The plugin should respond with a {@link PluginScheduleEntryPreScheduleTaskFinishedEvent} when pre-schedule tasks are complete. - *
- * Note: This is an EventBus subscriber method and requires the implementing - * plugin to be registered with the EventBus for it to be called. - * - * @param event The pre-schedule task event containing the plugin reference that should start pre-schedule tasks - * @param scriptSetupAndStartCallback Callback to execute after pre-schedule tasks complete (typically script setup and start) - */ - default public void executePreScheduleTasks(Runnable postTaskCallback) { - - - AbstractPrePostScheduleTasks prePostTasks = getPrePostScheduleTasks(); - if (prePostTasks != null) { - // Plugin has pre/post tasks and is under scheduler control - log.info("Plugin {} starting with pre-schedule tasks", this.getClass().getSimpleName()); - - try { - // Execute pre-schedule tasks with callback to start main script - prePostTasks.executePreScheduleTasks(() -> { - log.info("Pre-Schedule Tasks completed successfully for {}", this.getClass().getSimpleName()); - if (postTaskCallback != null) { - postTaskCallback.run(); // Execute script setup and start - } - // Report pre-schedule task completion - the scheduler will transition to RUNNING_PLUGIN state - Microbot.getEventBus().post(new PluginScheduleEntryPreScheduleTaskFinishedEvent( - (Plugin) this, ExecutionResult.SUCCESS, "Pre-schedule tasks completed successfully")); - }); - } catch (Exception e) { - log.error("Error during Pre-Schedule Tasks for {}", this.getClass().getSimpleName(), e); - // Report pre-schedule task failure - Microbot.getEventBus().post(new PluginScheduleEntryPreScheduleTaskFinishedEvent( - (Plugin) this, ExecutionResult.HARD_FAILURE, "Pre-schedule tasks failed: " + e.getMessage())); - } - } else { - // No pre-schedule tasks or not under scheduler control - execute callback immediately - log.info("Plugin {} has no pre-schedule tasks - executing callback immediately", this.getClass().getSimpleName()); - - if (postTaskCallback != null) { - postTaskCallback.run(); // Execute script setup,etc, post pre schedule tasks action - } - - // Report completion immediately - Microbot.getEventBus().post(new PluginScheduleEntryPreScheduleTaskFinishedEvent( - (Plugin) this, ExecutionResult.SUCCESS, "No pre-schedule tasks - callback executed successfully")); - } - } - - - /** - * Tests only the post-schedule tasks functionality. - * This method demonstrates how post-schedule tasks work and logs the results. - */ - default public void executePostScheduleTasks(Runnable postTaskExecutionCallback) { - log.info("Post-Schedule Tasks execution..."); - AbstractPrePostScheduleTasks prePostScheduleTasks = getPrePostScheduleTasks(); - if (prePostScheduleTasks == null) { - log.warn("PrePostScheduleTasks not initialized - cannot test"); - if(postTaskExecutionCallback!= null) postTaskExecutionCallback.run(); - return; - } - - try { - if (prePostScheduleTasks.isPostScheduleRunning()) { - log.warn("Post-Schedule Tasks are already running. Cannot start again."); - return; - } - // Execute only post-schedule tasks using the public API - prePostScheduleTasks.executePostScheduleTasks(() -> { - log.info("Post-Schedule Tasks completed successfully"); - if(postTaskExecutionCallback!= null) postTaskExecutionCallback.run(); - }); - } catch (Exception e) { - log.error("Error during Post-Schedule Tasks test", e); - } - } - - /** - * Allows a plugin to report that it has finished its task and is ready to be stopped. - * Use this method when your plugin has completed its primary task successfully or - * encountered a situation where it should be stopped even if the configured stop - * conditions haven't been met yet. - * - * @param reason A description of why the plugin is finished - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - */ - private String reportFinished_internal(String reason, boolean success) { - if ( this instanceof Plugin && !Microbot.isPluginEnabled((Plugin)this)){ - log.warn("Plugin {} is not enabled, cannot report finished", this.getClass().getSimpleName()); - return null; - }; - SchedulerPlugin schedulablePlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - boolean shouldStop = false; - if (schedulablePlugin == null) { - Microbot.log("\n SchedulerPlugin is not loaded. so stopping the current plugin.", Level.INFO); - shouldStop = true; - - }else{ - PluginScheduleEntry currentPlugin = schedulablePlugin.getCurrentPlugin(); - if (currentPlugin == null) { - Microbot.log("\n\t SchedulerPlugin is not running any plugin. so stopping the current plugin."); - shouldStop = true; - }else{ - if (currentPlugin.isRunning() && currentPlugin.getPlugin() != null && !currentPlugin.getPlugin().equals(this)) { - Microbot.log("\n\t Current running plugin running by the SchedulerPlugin is not the same as the one being stopped. Stopping current plugin."); - shouldStop = true; - } - } - } - String configGrpName = ""; - if(getConfigDescriptor()!=null){ - configGrpName = getConfigDescriptor().getGroup().value(); - } - boolean isSchedulerMode = AbstractPrePostScheduleTasks.isScheduleMode(this,configGrpName); - log.info("test if plugin is in:\n\t\tscheduler mode: {} -- should stop {}", isSchedulerMode, shouldStop); - String prefix = "Plugin [" + this.getClass().getSimpleName() + "] finished: "; - String reasonExt= reason == null ? prefix+"No reason provided" : prefix+reason; - if (!isSchedulerMode){ - // If plugin finished unsuccessfully, show a non-blocking notification dialog - if (!success) { - Microbot.log("\nPlugin [" + this.getClass().getSimpleName() + "] stopped with error: " + reasonExt, Level.ERROR); - Microbot.getClientThread().invokeLater(()->{ - try { - SwingUtilities.invokeAndWait(() -> { - // Find a parent frame to attach the dialog to - Component clientComponent = (Component)Microbot.getClient(); - Window window = SwingUtilities.getWindowAncestor(clientComponent); - // Create message with HTML for proper text wrapping - JLabel messageLabel = new JLabel("
" + - "Plugin [" + ((Plugin)this).getClass().getSimpleName() + - "] stopped: " + (reason != null ? reason : "No reason provided") + - ""); - // Show error message if starting failed - JOptionPane.showMessageDialog( - window, - messageLabel, - "Plugin Stopped", - JOptionPane.WARNING_MESSAGE - ); - - }); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - Microbot.log("Dialog display was interrupted: " + e.getMessage(), Level.WARN); - } catch (java.lang.reflect.InvocationTargetException e) { - Microbot.log("Error displaying plugin stopped dialog: " + e.getCause().getMessage(), Level.ERROR); - } - }); - - } - // Capture the plugin reference explicitly to avoid timing issues - final Plugin pluginToStop = (Plugin) this; - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(pluginToStop); - }); - return reasonExt; - }else{ - if (success) { - log.info(reasonExt); - } else { - log.error(reasonExt); - } - return reasonExt; - } - } - /** - * Reports that the plugin has finished its main task. - * - * @param reason A description of why the plugin finished - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - */ - default public void reportFinished(String reason, ExecutionResult result) { - String reasonExt = reportFinished_internal(reason, result.isSuccess()); - if (reasonExt == null) { - return; // Plugin is not enabled or scheduler is not running - } - Microbot.getEventBus().post(new PluginScheduleEntryMainTaskFinishedEvent( - (Plugin) this, // "this" will be the plugin instance - reasonExt, - result - ) - ); - } - - /** - * @deprecated Use {@link #reportFinished(String, ExecutionResult)} instead for granular result reporting - */ - @Deprecated - default public void reportFinished(String reason, boolean success) { - ExecutionResult result = success ? ExecutionResult.SUCCESS : ExecutionResult.HARD_FAILURE; - reportFinished(reason, result); - } - /** - * Reports that the plugin's post-schedule tasks have finished. - * - * @param reason A description of the completion - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - */ - default public void reportPostScheduleTaskFinished(String reason, ExecutionResult result) { - String reasonExt = reportFinished_internal(reason, result.isSuccess()); - if (reasonExt == null) { - return; // Plugin is not enabled or scheduler is not running - } - Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskFinishedEvent( - (Plugin) this, // "this" will be the plugin instance - result, - reasonExt - ) - ); - } - - /** - * @deprecated Use {@link #reportPostScheduleTaskFinished(String, ExecutionResult)} instead for granular result reporting - */ - @Deprecated - default public void reportPostScheduleTaskFinished(String reason, boolean success) { - ExecutionResult result = success ? ExecutionResult.SUCCESS : ExecutionResult.HARD_FAILURE; - reportPostScheduleTaskFinished(reason, result); - } - /** - * Reports that the plugin's pre-schedule tasks have finished. - * - * @param reason A description of the completion - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - */ - default public void reportPreScheduleTaskFinished(String reason, ExecutionResult result) { - if (!result.isSuccess()) { - reason = reportFinished_internal(reason, result.isSuccess()); - } - if (reason == null) { - return; // Plugin is not enabled or scheduler is not running - } - Microbot.getEventBus().post(new PluginScheduleEntryPreScheduleTaskFinishedEvent( - (Plugin) this, // "this" will be the plugin instance - result, - reason - ) - ); - } - - /** - * @deprecated Use {@link #reportPreScheduleTaskFinished(String, ExecutionResult)} instead for granular result reporting - */ - @Deprecated - default public void reportPreScheduleTaskFinished(String reason, boolean success) { - ExecutionResult result = success ? ExecutionResult.SUCCESS : ExecutionResult.HARD_FAILURE; - reportPreScheduleTaskFinished(reason, result); - } - - - /** - * Determines if this plugin can be forcibly terminated by the scheduler through a hard stop. - * - * A hard stop means the scheduler can immediately terminate the plugin's execution - * without waiting for the plugin to reach a safe stopping point. - * When false (default), the scheduler will only perform soft stops, allowing the plugin - * to terminate gracefully at designated checkpoints. - * - * @return true if this plugin supports being forcibly terminated, false otherwise - */ - default public boolean allowHardStop(){ - return false; - } - - /** - * Returns the lock condition that can be used to prevent the plugin from being stopped. - * The lock condition is stored in the plugin's stop condition structure and can be - * toggled to prevent the plugin from being stopped during critical operations. - * - * @return The lock condition for this plugin, or null if not present - */ - default LockCondition getLockCondition(Condition stopConditions) { - - if (stopConditions != null) { - return findLockCondition(stopConditions); - } - return null; - } - - /** - * Recursively searches for a LockCondition within a logical condition structure. - * - * @param condition The condition to search within - * @return The first LockCondition found, or null if none exists - */ - default LockCondition findLockCondition(Condition condition) { - if (condition instanceof LockCondition) { - return (LockCondition) condition; - } - - if (condition instanceof LogicalCondition) { - ListallLockCondtions = ((LogicalCondition)condition).findAllLockConditions(); - // todo think of only allow one lock condition per plugin in the nested structure... because more makes no sense? - - return allLockCondtions.isEmpty() ? null : allLockCondtions.get(0); - - - } - - return null; - } - - /** - * Checks if the plugin is currently locked from being stopped. - * - * @return true if the plugin is locked, false otherwise - */ - default boolean isLocked(Condition stopConditions) { - LockCondition lockCondition = getLockCondition(stopConditions); - return lockCondition != null && lockCondition.isLocked(); - } - - /** - * Locks the plugin, preventing it from being stopped regardless of other conditions. - * Use this during critical operations where the plugin should not be interrupted. - * - * @return true if the plugin was successfully locked, false if no lock condition exists - */ - default boolean lock(Condition stopConditions) { - LockCondition lockCondition = getLockCondition(stopConditions); - if (lockCondition != null) { - lockCondition.lock(); - return true; - } - return false; - } - - /** - * Unlocks the plugin, allowing it to be stopped when stop conditions are met. - * - * @return true if the plugin was successfully unlocked, false if no lock condition exists - */ - default boolean unlock(Condition stopConditions) { - LockCondition lockCondition = getLockCondition(stopConditions); - if (lockCondition != null) { - lockCondition.unlock(); - return true; - } - return false; - } - - /** - * Toggles the lock state of the plugin. - * - * @return The new lock state (true if locked, false if unlocked), or null if no lock condition exists - */ - default Boolean toggleLock(Condition anyCondition) { - if (anyCondition == null) { - return null; // No stop conditions defined - } - LockCondition lockCondition = getLockCondition( anyCondition); - - if (lockCondition != null) { - return lockCondition.toggleLock(); - } - return null; - } - - /** - * Unlocks all lock conditions in both start and stop conditions. - * This utility function provides defensive unlocking of all lock conditions - * to prevent plugins from getting stuck in locked states. - */ - default void unlockAllConditions() { - unlockAllStartConditions(); - unlockAllStopConditions(); - } - - /** - * Unlocks all lock conditions in the start conditions. - * Recursively searches through the condition structure to find and unlock all lock conditions. - */ - default void unlockAllStartConditions() { - LogicalCondition startCondition = getStartCondition(); - if (startCondition != null) { - List allLockConditions = startCondition.findAllLockConditions(); - for (LockCondition lockCondition : allLockConditions) { - if (lockCondition.isLocked()) { - lockCondition.unlock(); - } - } - } - } - - /** - * Unlocks all lock conditions in the stop conditions. - * Recursively searches through the condition structure to find and unlock all lock conditions. - */ - default void unlockAllStopConditions() { - LogicalCondition stopCondition = getStopCondition(); - if (stopCondition != null) { - List allLockConditions = stopCondition.findAllLockConditions(); - for (LockCondition lockCondition : allLockConditions) { - if (lockCondition.isLocked()) { - lockCondition.unlock(); - } - } - } - } - - /** - * Gets all lock conditions from both start and stop conditions. - * - * @return List of all lock conditions found in the plugin's condition structure - */ - default List getAllLockConditions() { - List allLocks = new ArrayList<>(); - - // get start condition locks - LogicalCondition startCondition = getStartCondition(); - if (startCondition != null) { - allLocks.addAll(startCondition.findAllLockConditions()); - } - - // get stop condition locks - LogicalCondition stopCondition = getStopCondition(); - if (stopCondition != null) { - allLocks.addAll(stopCondition.findAllLockConditions()); - } - - return allLocks; - } - - /** - * Provides the configuration descriptor for scheduler integration and per-entry configuration management. - * - * Purpose: Enables the {@link SchedulerPlugin} to manage separate configuration instances - * for each {@link net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry}. - *
- * Schedule Mode Detection: {@link AbstractPrePostScheduleTasks} uses this method: - *
- *
- *- Non-null return → Scheduler mode (managed by SchedulerPlugin)
- *- Null return → Manual mode (direct plugin execution)
- *- * Benefits: Same plugin, different configurations per schedule slot; centralized management through scheduler UI. - * - * @return The configuration descriptor for per-entry configuration, or null if not supported - * @see net.runelite.client.config.ConfigDescriptor - * @see SchedulerPlugin - * @see AbstractPrePostScheduleTasks#isScheduleMode() - */ - default public ConfigDescriptor getConfigDescriptor(){ - // Default implementation returns null, subclasses should override if they support configuration - return null; - } - - /** - * Returns the pre/post schedule tasks instance for scheduler integration. - *
- * Purpose: Provides setup/cleanup tasks and schedule mode detection for the {@link SchedulerPlugin}. - *
- * Integration: SchedulerPlugin uses this to: - *
- *
- *- Execute pre-schedule tasks before plugin start
- *- Execute post-schedule tasks during plugin shutdown
- *- Detect scheduler vs manual operation mode
- *- * Schedule Mode Detection: Uses {@link #getConfigDescriptor()} and event context - * to determine if plugin is under scheduler control. - * - * @return The pre/post schedule tasks instance, or null if not supported - * @see AbstractPrePostScheduleTasks - * @see AbstractPrePostScheduleTasks#isScheduleMode() - * @see SchedulerPlugin - */ - @Nullable - default public AbstractPrePostScheduleTasks getPrePostScheduleTasks(){ - // Default implementation returns null, subclasses should override if they support pre/post tasks - return null; - } - - /** - * Gets the time until the next scheduled plugin will run. - * This method checks the SchedulerPlugin for the upcoming plugin and calculates - * the duration until it's scheduled to execute. - * - * @return Optional containing the duration until the next plugin runs, - * or empty if no plugin is upcoming or time cannot be determined - */ - default Optional
getTimeUntilUpComingScheduledPlugin() { - try { - return SchedulerPluginUtil.getTimeUntilUpComingScheduledPlugin(); - - } catch (Exception e) { - Microbot.log("Error getting time until next scheduled plugin: " + e.getMessage(), Level.ERROR); - return Optional.empty(); - } - } - - /** - * Gets information about the next scheduled plugin. - * This method provides both the plugin entry and the time until it runs. - * - * @return Optional containing a formatted string with plugin name and time until run, - * or empty if no plugin is upcoming - */ - default Optional getUpComingScheduledPluginInfo() { - try { - return SchedulerPluginUtil.getUpComingScheduledPluginInfo(); - } catch (Exception e) { - Microbot.log("Error getting next scheduled plugin info: " + e.getMessage(), Level.ERROR); - return Optional.empty(); - } - } - - /** - * Gets the next scheduled plugin entry with its complete information. - * This provides access to the full PluginScheduleEntry object. - * - * @return Optional containing the next scheduled plugin entry, - * or empty if no plugin is upcoming - */ - default Optional getNextUpComingPluginScheduleEntry() { - try { - return SchedulerPluginUtil.getNextUpComingPluginScheduleEntry(); - } catch (Exception e) { - Microbot.log("Error getting next scheduled plugin entry: " + e.getMessage(), Level.ERROR); - return Optional.empty(); - } - } - /** - * Allows external registration of custom requirements to this plugin's pre/post schedule tasks. - * Plugin developers can control whether and how to integrate external requirements. - * - * @param requirement The requirement to add - * @param TaskContext The context in which this requirement should be fulfilled (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @return true if the requirement was accepted and registered, false if rejected - */ - default boolean addCustomRequirement(Requirement requirement, - TaskContext taskContext) { - AbstractPrePostScheduleTasks tasks = getPrePostScheduleTasks(); - if (tasks != null) { - return tasks.addCustomRequirement(requirement, taskContext); - } - return false; // No pre/post tasks available, cannot register custom requirements - } - - /** - * Checks if this plugin supports external custom requirement registration. - * - * @return true if custom requirements can be added, false otherwise - */ - default boolean supportsCustomRequirements() { - return getPrePostScheduleTasks() != null; - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/Condition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/Condition.java deleted file mode 100755 index 1b44c89ea1a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/Condition.java +++ /dev/null @@ -1,304 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition; - -import java.time.Duration; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.Optional; - -import net.runelite.api.events.AnimationChanged; -import net.runelite.api.events.ChatMessage; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.GroundObjectDespawned; -import net.runelite.api.events.GroundObjectSpawned; -import net.runelite.api.events.HitsplatApplied; -import net.runelite.api.events.InteractingChanged; -import net.runelite.api.events.ItemContainerChanged; -import net.runelite.api.events.ItemDespawned; -import net.runelite.api.events.ItemSpawned; -import net.runelite.api.events.MenuOptionClicked; -import net.runelite.api.events.NpcChanged; -import net.runelite.api.events.NpcDespawned; -import net.runelite.api.events.NpcSpawned; -import net.runelite.api.events.StatChanged; -import net.runelite.api.events.VarbitChanged; - -/** - * Base interface for script execution conditions. - * Provides common functionality for condition checking and configuration. - */ - -public interface Condition { - - public static String getVersion(){ - return getVersion(); - } - /** - * Checks if the condition is currently met - * @return true if condition is satisfied, false otherwise - */ - boolean isSatisfied(); - - /** - * Returns a human-readable description of this condition - * @return description string - */ - String getDescription(); - - /** - * Returns a detailed description of the level condition with additional status information - */ - String getDetailedDescription(); - - /** - * Gets the estimated time until this condition will be satisfied. - * This provides a duration-based estimate for when the condition might become true. - * - * For time-based conditions, this calculates the duration until the next trigger time. - * For satisfied conditions, this returns Duration.ZERO. - * For conditions where the satisfaction time cannot be determined, this returns Optional.empty(). - * - * @return Optional containing the estimated duration until satisfaction, or empty if not determinable - */ - default Optional getEstimatedTimeWhenIsSatisfied() { - // If the condition is already satisfied, return zero duration - if (isSatisfied()) { - return Optional.of(Duration.ZERO); - } - - // Try to get trigger time and calculate duration - Optional triggerTime = getCurrentTriggerTime(); - if (triggerTime.isPresent()) { - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - Duration duration = Duration.between(now, triggerTime.get()); - - // Ensure we don't return negative durations - if (duration.isNegative()) { - return Optional.of(Duration.ZERO); - } - return Optional.of(duration); - } - - // Default implementation for conditions that can't estimate satisfaction time - return Optional.empty(); - } - /** - * Gets the next time this condition will be satisfied. - * For time-based conditions, this returns the actual next trigger time. - * For satisfied conditions, this returns a time slightly in the past. - * For non-time conditions that aren't satisfied, this returns Optional.empty(). - * - * @return Optional containing the next trigger time, or empty if not applicable - */ - default Optional getCurrentTriggerTime() { - // If the condition is already satisfied, return a time 1 second in the past - if (isSatisfied()) { - return Optional.of(ZonedDateTime.now(ZoneId.systemDefault()).minusSeconds(1)); - } - // Default implementation for non-time conditions that aren't satisfied - return Optional.empty(); - } - /** - * Returns the type of this condition - * @return ConditionType enum value - */ - ConditionType getType(); - /** - * Resets the condition to its initial state. - * - * For example, in a time-based condition, calling this method - * will update the reference timestamp used for calculating - * time intervals. - */ - default void reset(){ - reset(false); - } - - void reset (boolean randomize); - /** - * Performs a complete reset of the condition, including both the current trigger - * and all internal state tracking variables. - *
- * Unlike the standard {@link #reset()} method, this will clear accumulated state - * such as: - *
- *
- *- Maximum trigger counters
- *- Daily/periodic usage limits
- *- Historical tracking data
- *- * For example, if a condition can only be triggered a maximum number of times - * or has daily usage limits, this method will reset those tracking variables as well. - *
- * This method is equivalent to calling {@link #reset(boolean) reset(true)}. - */ - default void hardReset(){ - reset(true); - } - - default void onGameStateChanged(GameStateChanged gameStateChanged) { - // This event handler is called whenever the game state changes - // Useful for conditions that depend on the game state (e.g., logged in, logged out) - } - default void onStatChanged(StatChanged event) { - // This event handler is called whenever a skill stat changes - // Useful for skill-based conditions - } - default void onItemContainerChanged(ItemContainerChanged event) { - // This event handler is called whenever inventory or bank contents change - // Useful for item-based conditions - } - default void onGameTick(GameTick gameTick) { - // This event handler is called every game tick (approximately once per 0.6 seconds) - // Useful for time-based conditions - } - default void onNpcChanged(NpcChanged event){ - // This event handler is called whenever an NPC changes - // Useful for NPC-based conditions - } - default void onNpcSpawned(NpcSpawned npcSpawned){ - // This event handler is called whenever an NPC spawns - // Useful for NPC-based conditions - } - default void onNpcDespawned(NpcDespawned npcDespawned){ - // This event handler is called whenever an NPC despawns - // Useful for NPC-based conditions - } - /** - * Called when a ground item is spawned in the game world - */ - default void onGroundObjectSpawned(GroundObjectSpawned event) { - // Optional implementation - } - - /** - * Called when a ground item is despawned from the game world - */ - default void onGroundObjectDespawned(GroundObjectDespawned event) { - // Optional implementation - } - /** - * Called when an item is spawned in the game world - */ - default void onItemSpawned(ItemSpawned event) { - // Optional implementation - } - /** - * Called when an item is despawned from the game world - */ - default void onItemDespawned(ItemDespawned event){ - // This event handler is called whenever an item despawns - // Useful for item-based conditions - } - - /** - * Called when a menu option is clicked - */ - default void onMenuOptionClicked(MenuOptionClicked event) { - // Optional implementation - } - - /** - * Called when a chat message is received - */ - default void onChatMessage(ChatMessage event) { - // Optional implementation - } - - /** - * Called when a hitsplat is applied to a character - */ - default void onHitsplatApplied(HitsplatApplied event) { - // Optional implementation - } - default void onVarbitChanged(VarbitChanged event){ - // Optional implementation - } - default void onInteractingChanged(InteractingChanged event){ - // Optional implementation - } - default void onAnimationChanged(AnimationChanged event) { - // Optional implementation - } - /** - * Returns the progress percentage for this condition (0-100). - * For simple conditions that are either met or not met, this will return 0 or 100. - * For conditions that track progress (like XP conditions), this will return a value between 0 and 100. - * - * @return Percentage of condition completion (0-100) - */ - default double getProgressPercentage() { - // Default implementation returns 0 for not met, 100 for met - return isSatisfied() ? 100.0 : 0.0; - } - - /** - * Gets the total number of leaf conditions in this condition tree. - * For simple conditions, this is 1. - * For logical conditions, this is the sum of all contained conditions' counts. - * - * @return Total number of leaf conditions in this tree - */ - default int getTotalConditionCount() { - return 1; // Simple conditions count as 1 - } - - /** - * Gets the number of leaf conditions that are currently met. - * For simple conditions, this is 0 or 1. - * For logical conditions, this is the sum of all contained conditions' met counts. - * - * @return Number of met leaf conditions in this tree - */ - default int getMetConditionCount() { - return isSatisfied() ? 1 : 0; // Simple conditions return 1 if met, 0 otherwise - } - - /** - * Pauses this condition, preventing it from being satisfied until resumed. - * While paused, the condition evaluation is suspended. - * For time-based conditions, the pause duration will be tracked to adjust trigger times accordingly. - * For event-based conditions, events may still be processed but won't trigger satisfaction. - */ - public void pause(); - - - /** - * Resumes this condition, allowing it to be satisfied again. - * Reactivates condition evaluation that was previously suspended by pause(). - * For time-based conditions, trigger times will be adjusted by the pause duration. - * For event-based conditions, satisfaction evaluation will resume with the current state. - */ - public void resume(); - - - /** - * Generates detailed status information for this condition and any nested conditions. - * - * @param indent Current indentation level for nested formatting - * @param showProgress Whether to include progress percentage in the output - * @return A string with detailed status information - */ - default String getStatusInfo(int indent, boolean showProgress) { - StringBuilder sb = new StringBuilder(); - - String indentation = " ".repeat(indent); - boolean isSatisfied = isSatisfied(); - - sb.append(indentation) - .append(getDescription()) - .append(" [") - .append(isSatisfied ? "SATISFIED" : "NOT SATISFIED") - .append("]"); - - if (showProgress) { - double progress = getProgressPercentage(); - if (progress > 0 && progress < 100) { - sb.append(" (").append(String.format("%.1f%%", progress)).append(")"); - } - } - - return sb.toString(); - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionManager.java deleted file mode 100755 index b0ae88c12de..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionManager.java +++ /dev/null @@ -1,2691 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition; - -import java.time.Duration; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.AnimationChanged; -import net.runelite.api.events.ChatMessage; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.GroundObjectDespawned; -import net.runelite.api.events.GroundObjectSpawned; -import net.runelite.api.events.HitsplatApplied; -import net.runelite.api.events.InteractingChanged; -import net.runelite.api.events.ItemContainerChanged; -import net.runelite.api.events.ItemDespawned; -import net.runelite.api.events.ItemSpawned; -import net.runelite.api.events.MenuOptionClicked; -import net.runelite.api.events.NpcChanged; -import net.runelite.api.events.NpcDespawned; -import net.runelite.api.events.NpcSpawned; -import net.runelite.api.events.StatChanged; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.eventbus.EventBus; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.AndCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.NotCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.OrCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.enums.UpdateOption; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ResourceCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; - -/** - * Manages hierarchical logical conditions for plugins, handling both user-defined and plugin-defined conditions. - *
- * The ConditionManager provides a framework for defining, evaluating, and monitoring complex logical structures - * of conditions, supporting both AND and OR logical operators. It maintains two separate condition hierarchies: - *
- *
- *- Plugin conditions: Defined by the plugin itself, immutable by users
- *- User conditions: User-configurable conditions that can be modified at runtime
- *- * When evaluating the full condition set, plugin and user conditions are combined with AND logic - * (both must be satisfied). Within each hierarchy, conditions can be organized with custom logical structures. - *
- * Features include: - *
- *
- *- Complex logical condition hierarchies with AND/OR operations
- *- Time-based condition scheduling and monitoring
- *- Event-based condition updates via RuneLite event bus integration
- *- Watchdog system for periodic condition structure updates
- *- Progress tracking toward condition satisfaction
- *- Single-trigger time conditions for one-time events
- *- * This class implements AutoCloseable to ensure proper resource cleanup. Be sure to call close() - * when the condition manager is no longer needed to prevent memory leaks and resource exhaustion. - * - * @see LogicalCondition - * @see Condition - * @see TimeCondition - * @see SingleTriggerTimeCondition - */ -@Slf4j -public class ConditionManager implements AutoCloseable { - - /** - * Shared thread pool for condition watchdog tasks across all ConditionManager instances. - * Uses daemon threads to prevent blocking application shutdown. - */ - private transient static final ScheduledExecutorService SHARED_WATCHDOG_EXECUTOR = - Executors.newScheduledThreadPool(2, r -> { - Thread t = new Thread(r, "ConditionWatchdog"); - t.setDaemon(true); - return t; - }); - - /** - * Keeps track of all scheduled futures created by this manager's watchdog system. - * Used to ensure all scheduled tasks are properly canceled when the manager is closed. - */ - private transient final List
> watchdogFutures = - new ArrayList<>(); - - /** - * Plugin-defined logical condition structure. Contains conditions defined by the plugin itself. - * This is combined with user conditions using AND logic when evaluating the full condition set. - */ - private LogicalCondition pluginCondition = new OrCondition(); - - /** - * User-defined logical condition structure. Contains conditions defined by the user. - * This is combined with plugin conditions using AND logic when evaluating the full condition set. - */ - @Getter - private LogicalCondition userLogicalCondition; - - /** - * Reference to the event bus for registering condition event listeners. - */ - private final EventBus eventBus; - - /** - * Tracks whether this manager has registered its event listeners with the event bus. - */ - private boolean eventsRegistered = false; - - /** - * Indicates whether condition watchdog tasks are currently active. - */ - private boolean watchdogsRunning = false; - - /** - * Stores the current update strategy for watchdog condition updates. - * Controls how new conditions are merged with existing ones during watchdog checks. - */ - private UpdateOption currentWatchdogUpdateOption = UpdateOption.SYNC; - - /** - * The current interval in milliseconds between watchdog condition checks. - */ - private long currentWatchdogInterval = 10000; // Default interval - - /** - * The current supplier function that provides updated conditions for watchdog checks. - */ - private Supplier currentWatchdogSupplier = null; - - /** - * Creates a new condition manager with default settings. - * Initializes the user logical condition as an AND condition (all conditions must be met). - */ - public ConditionManager() { - this.eventBus = Microbot.getEventBus(); - userLogicalCondition = new AndCondition(); - } - - /** - * Sets the plugin-defined logical condition structure. - * - * @param condition The logical condition to set as the plugin structure - */ - public void setPluginCondition(LogicalCondition condition) { - pluginCondition = condition; - } - - /** - * Gets the plugin-defined logical condition structure. - * - * @return The current plugin logical condition, or a default OrCondition if none was set - */ - public LogicalCondition getPluginCondition() { - return pluginCondition; - } - - /** - * Returns a combined list of all conditions from both plugin and user condition structures. - * Plugin conditions are listed first, followed by user conditions. - * - * @return A list containing all conditions managed by this ConditionManager - */ - public List getConditions() { - List conditions = new ArrayList<>(); - if (pluginCondition != null) { - conditions.addAll(pluginCondition.getConditions()); - } - conditions.addAll(userLogicalCondition.getConditions()); - return conditions; - } - /** - * Checks if any conditions exist in the manager. - * - * @return true if at least one condition exists in either plugin or user condition structures - */ - public boolean hasConditions() { - return !getConditions().isEmpty(); - } - /** - * Retrieves all time-based conditions from both plugin and user condition structures. - * Uses the LogicalCondition.findTimeConditions method to recursively find all TimeCondition - * instances throughout the nested logical structure. - * - * @return A list of all TimeCondition instances managed by this ConditionManager - */ - public List getAllTimeConditions() { - List timeConditions = new ArrayList<>(); - - // Add time conditions from user logical structure - timeConditions.addAll(getUserTimeConditions()); - - // Add time conditions from plugin logical structure - timeConditions.addAll(getPluginTimeConditions()); - - return timeConditions; - } - - /** - * Retrieves time-based conditions from user condition structure only. - * Uses the LogicalCondition.findTimeConditions method to recursively find all TimeCondition - * instances throughout the nested user logical structure. - * - * @return A list of TimeCondition instances from user conditions - */ - public List getUserTimeConditions() { - List timeConditions = new ArrayList<>(); - - // Get time conditions from user logical structure - if (userLogicalCondition != null) { - List userTimeConditions = userLogicalCondition.findTimeConditions(); - - for (Condition condition : userTimeConditions) { - if (condition instanceof TimeCondition) { - timeConditions.add((TimeCondition) condition); - } - } - } - - return timeConditions; - } - - /** - * Retrieves time-based conditions from plugin condition structure only. - * Uses the LogicalCondition.findTimeConditions method to recursively find all TimeCondition - * instances throughout the nested plugin logical structure. - * - * @return A list of TimeCondition instances from plugin conditions - */ - public List getPluginTimeConditions() { - List timeConditions = new ArrayList<>(); - - // Get time conditions from plugin logical structure - if (pluginCondition != null) { - List pluginTimeConditions = pluginCondition.findTimeConditions(); - - for (Condition condition : pluginTimeConditions) { - if (condition instanceof TimeCondition) { - timeConditions.add((TimeCondition) condition); - } - } - } - - return timeConditions; - } - - /** - * Retrieves all time-based conditions from both plugin and user condition structures. - * This is an alias for getAllTimeConditions() for backward compatibility. - * - * @return A list of all TimeCondition instances managed by this ConditionManager - * @deprecated Use getAllTimeConditions() instead - */ - public List getTimeConditions() { - return getAllTimeConditions(); - } - - - /** - * Retrieves all non-time-based conditions from both plugin and user condition structures. - * Uses the LogicalCondition.findNonTimeConditions method to recursively find all non-TimeCondition - * instances throughout the nested logical structure. - * - * @return A list of all non-TimeCondition instances managed by this ConditionManager - */ - public List getNonTimeConditions() { - List nonTimeConditions = new ArrayList<>(); - - // Get non-time conditions from user logical structure - if (userLogicalCondition != null) { - List userNonTimeConditions = userLogicalCondition.findNonTimeConditions(); - nonTimeConditions.addAll(userNonTimeConditions); - } - - // Get non-time conditions from plugin logical structure - if (pluginCondition != null) { - List pluginNonTimeConditions = pluginCondition.findNonTimeConditions(); - nonTimeConditions.addAll(pluginNonTimeConditions); - } - - return nonTimeConditions; - } - - /** - * Checks if the condition manager contains only time-based conditions. - * - * @return true if all conditions in the manager are TimeConditions, false otherwise - */ - public boolean hasOnlyTimeConditions() { - return getNonTimeConditions().isEmpty(); - } - /** - * Returns the user logical condition structure. - * - * @return The logical condition structure containing user-defined conditions - */ - public LogicalCondition getUserCondition() { - return userLogicalCondition; - } - - /** - * Removes all user-defined conditions while preserving the logical structure. - * This clears the user condition list without changing the logical operator (AND/OR). - */ - public void clearUserConditions() { - userLogicalCondition.getConditions().clear(); - } - /** - * Evaluates if all conditions are currently satisfied, respecting the logical structure. - * - * This method first checks if user conditions are met according to their logical operator (AND/OR). - * If plugin conditions exist, they must also be satisfied (always using AND logic between - * user and plugin conditions). - * - * @return true if all required conditions are satisfied based on the logical structure - */ - public boolean areAllConditionsMet() { - - return areUserConditionsMet() && arePluginConditionsMet(); - } - public boolean arePluginConditionsMet() { - if (pluginCondition != null && !pluginCondition.getConditions().isEmpty()) { - return pluginCondition.isSatisfied(); - } - return true; - } - - public boolean areUserConditionsMet() { - if (userLogicalCondition != null && !userLogicalCondition.getConditions().isEmpty()) { - return userLogicalCondition.isSatisfied(); - } - return true; - } - /** - * Returns a list of conditions that were defined by the user (not plugin-defined). - * This method only retrieves conditions from the user logical condition structure, - * not from the plugin condition structure. - * - * @return List of user-defined conditions, or an empty list if no user logical condition exists - */ - public List
getUserConditions() { - if (userLogicalCondition == null) { - return new ArrayList<>(); - } - - return userLogicalCondition.getConditions(); - } - public List getPluginConditions() { - if (pluginCondition == null) { - return new ArrayList<>(); - } - - return pluginCondition.getConditions(); - } - /** - * Registers this condition manager to receive RuneLite events. - * Event listeners are registered with the event bus to allow conditions - * to update their state based on game events. This method is idempotent - * and will not register the same listeners twice. - */ - public void registerEvents() { - if (eventsRegistered) { - return; - } - eventBus.register(this); - eventsRegistered = true; - } - - /** - * Unregisters this condition manager from receiving RuneLite events. - * This removes all event listeners from the event bus that were previously registered. - * This method is idempotent and will do nothing if events are not currently registered. - */ - public void unregisterEvents() { - if (!eventsRegistered) { - return; - } - eventBus.unregister(this); - eventsRegistered = false; - } - - - /** - * Sets the user logical condition to require ALL conditions to be met (AND logic). - * This creates a new AndCondition to replace the existing user logical condition. - */ - public void setRequireAll() { - userLogicalCondition = new AndCondition(); - setUserLogicalCondition(userLogicalCondition); - - } - - /** - * Sets the user logical condition to require ANY condition to be met (OR logic). - * This creates a new OrCondition to replace the existing user logical condition. - */ - public void setRequireAny() { - userLogicalCondition = new OrCondition(); - setUserLogicalCondition(userLogicalCondition); - } - - - - /** - * Generates a human-readable description of the current condition structure. - * The description includes the logical operator type (ANY/ALL) and descriptions - * of all user conditions. If plugin conditions exist, those are appended as well. - * - * @return A string representation of the condition structure - */ - public String getDescription() { - - - StringBuilder sb; - if (requiresAny()){ - sb = new StringBuilder("ANY of: ("); - }else{ - sb = new StringBuilder("ALL of: ("); - } - List userConditions = userLogicalCondition.getConditions(); - if (userConditions.isEmpty()) { - sb.append("No conditions"); - }else{ - for (int i = 0; i < userConditions.size(); i++) { - if (i > 0) sb.append(" OR "); - sb.append(userConditions.get(i).getDescription()); - } - } - sb.append(")"); - - if ( this.pluginCondition!= null) { - sb.append(" AND : "); - sb.append(this.pluginCondition.getDescription()); - } - return sb.toString(); - - } - - /** - * Checks if the user logical condition requires all conditions to be met (AND logic). - * - * @return true if the user logical condition is an AndCondition, false otherwise - */ - public boolean userConditionRequiresAll() { - - return userLogicalCondition instanceof AndCondition; - } - /** - * Checks if the user logical condition requires any condition to be met (OR logic). - * - * @return true if the user logical condition is an OrCondition, false otherwise - */ - public boolean userConditionRequiresAny() { - return userLogicalCondition instanceof OrCondition; - } - /** - * Checks if the full logical structure (combining user and plugin conditions) - * requires all conditions to be met (AND logic). - * - * @return true if the full logical condition is an AndCondition, false otherwise - */ - public boolean requiresAll() { - return this.getFullLogicalCondition() instanceof AndCondition; - } - /** - * Checks if the full logical structure (combining user and plugin conditions) - * requires any condition to be met (OR logic). - * - * @return true if the full logical condition is an OrCondition, false otherwise - */ - public boolean requiresAny() { - return this.getFullLogicalCondition() instanceof OrCondition; - } - /** - * Resets all conditions in both user and plugin logical structures to their initial state. - * This method calls the reset() method on all conditions. - */ - public void reset() { - resetUserConditions(); - resetPluginConditions(); - } - - /** - * Resets all conditions in both user and plugin logical structures with an option to randomize. - * - * @param randomize If true, conditions will be reset with randomized initial values where applicable - */ - public void reset(boolean randomize) { - resetUserConditions(randomize); - resetPluginConditions(randomize); - } - - /** - * Resets only user conditions to their initial state. - */ - public void resetUserConditions() { - if (userLogicalCondition != null) { - userLogicalCondition.reset(); - } - } - - /** - * Resets only user conditions with an option to randomize. - * - * @param randomize If true, conditions will be reset with randomized initial values where applicable - */ - public void resetUserConditions(boolean randomize) { - if (userLogicalCondition != null) { - userLogicalCondition.reset(randomize); - } - - } - public void hardResetUserConditions() { - if (userLogicalCondition != null) { - userLogicalCondition.hardReset(); - } - } - - /** - * Resets only plugin conditions to their initial state. - */ - public void resetPluginConditions() { - if (pluginCondition != null) { - pluginCondition.reset(); - } - } - - /** - * Resets only plugin conditions with an option to randomize. - * - * @param randomize If true, conditions will be reset with randomized initial values where applicable - */ - public void resetPluginConditions(boolean randomize) { - if (pluginCondition != null) { - pluginCondition.reset(randomize); - } - } - - /** - * Checks if a condition is a plugin-defined condition that shouldn't be edited by users. - */ - public boolean isPluginDefinedCondition(Condition condition) { - // If there are no plugin-defined conditions, return false - if (pluginCondition == null) { - return false; - } - if (condition instanceof LogicalCondition) { - // If the condition is a logical condition, check if it's part of the plugin condition - if (pluginCondition.equals(condition)) { - return true; - } - } - // Checfk if the condition is contained in the plugin condition hierarchy - return pluginCondition.contains(condition); - } - - - - /** - * Recursively searches for and removes a condition from nested logical conditions. - * - * @param parent The logical condition to search within - * @param target The condition to remove - * @return true if the condition was found and removed, false otherwise - */ - private boolean removeFromNestedCondition(LogicalCondition parent, Condition target) { - // Search each child of the parent logical condition - for (int i = 0; i < parent.getConditions().size(); i++) { - Condition child = parent.getConditions().get(i); - - // If this child is itself a logical condition, search within it - if (child instanceof LogicalCondition) { - LogicalCondition logicalChild = (LogicalCondition) child; - - // First check if the target is a direct child of this logical condition - if (logicalChild.getConditions().remove(target)) { - // If removing the condition leaves the logical condition empty, remove it too - if (logicalChild.getConditions().isEmpty()) { - parent.getConditions().remove(i); - } - return true; - } - - // If not a direct child, recurse into the logical child - if (removeFromNestedCondition(logicalChild, target)) { - // If removing the condition leaves the logical condition empty, remove it too - parent.getConditions().remove(i); - return true; - } - } - // Special case for NotCondition - else if (child instanceof NotCondition) { - NotCondition notChild = (NotCondition) child; - - // If the NOT condition wraps our target, remove the whole NOT condition - if (notChild.getCondition() == target) { - parent.getConditions().remove(i); - return true; - } - - // If the NOT condition wraps a logical condition, search within that - if (notChild.getCondition() instanceof LogicalCondition) { - LogicalCondition wrappedLogical = (LogicalCondition) notChild.getCondition(); - if (removeFromNestedCondition(wrappedLogical, target)) { - // If removing the condition leaves the logical condition empty, remove the NOT condition too - parent.getConditions().remove(i); - } - } - } - } - - return false; - } - - /** - * Sets the user's logical condition structure - * - * @param logicalCondition The logical condition to set as the user structure - */ - public void setUserLogicalCondition(LogicalCondition logicalCondition) { - this.userLogicalCondition = logicalCondition; - } - - /** - * Gets the user's logical condition structure - * - * @return The current user logical condition, or null if none exists - */ - public LogicalCondition getUserLogicalCondition() { - return this.userLogicalCondition; - } - - - public boolean addToLogicalStructure(LogicalCondition parent, Condition toAdd) { - // Try direct addition first - if (parent.getConditions().add(toAdd)) { - return true; - } - - // Try to add to child logical conditions - for (Condition child : parent.getConditions()) { - if (child instanceof LogicalCondition) { - if (addToLogicalStructure((LogicalCondition) child, toAdd)) { - return true; - } - } - } - - return false; - } - - /** - * Recursively removes a condition from a logical structure - */ - public boolean removeFromLogicalStructure(LogicalCondition parent, Condition toRemove) { - // Try direct removal first - if (parent.getConditions().remove(toRemove)) { - return true; - } - - // Try to remove from child logical conditions - for (Condition child : parent.getConditions()) { - if (child instanceof LogicalCondition) { - if (removeFromLogicalStructure((LogicalCondition) child, toRemove)) { - return true; - } - } - } - - return false; - } - - /** - * Makes sure there's a valid user logical condition to work with - */ - private void ensureUserLogicalExists() { - if (userLogicalCondition == null) { - userLogicalCondition = new AndCondition(); - } - } - - /** - * Checks if the condition exists in either user or plugin logical structures - */ - public boolean containsCondition(Condition condition) { - ensureUserLogicalExists(); - - // Check user conditions - if (userLogicalCondition.contains(condition)) { - return true; - } - - // Check plugin conditions - return pluginCondition != null && pluginCondition.contains(condition); - } - - - - /** - * Adds a condition to the specified logical condition, or to the user root if none specified - */ - public void addConditionToLogical(Condition condition, LogicalCondition targetLogical) { - ensureUserLogicalExists(); - // find if the user logical condition contains the target logical condition - if ( targetLogical != userLogicalCondition && (targetLogical != null && !userLogicalCondition.contains(targetLogical))) { - log.warn("Target logical condition not found in user logical structure"); - return; - } - // check if condition already exists in logical structure - if (targetLogical != null && targetLogical.contains(condition)) { - log.warn("Condition already exists in logical structure"); - return; - } - // If no target specified, add to user root - if (targetLogical == null) { - userLogicalCondition.addCondition(condition); - return; - } - - // Otherwise, add to the specified logical - targetLogical.addCondition(condition); - } - - /** - * Adds a condition to the user logical root - */ - public void addUserCondition(Condition condition) { - addConditionToLogical(condition, userLogicalCondition); - } - - /** - * Removes a condition from any location in the logical structure - */ - public boolean removeCondition(Condition condition) { - ensureUserLogicalExists(); - - // Don't allow removing plugin conditions - if (isPluginDefinedCondition(condition)) { - log.warn("Attempted to remove a plugin-defined condition"); - return false; - } - if (condition instanceof LogicalCondition) { - // If the condition is a logical condition, check if it's part of the user logical structure - if (userLogicalCondition.equals(condition)) { - log.warn("Attempted to remove the user logical condition itself"); - userLogicalCondition = new AndCondition(); - } - } - // Remove from user logical structure - if (userLogicalCondition.removeCondition(condition)) { - return true; - } - - log.warn("Condition not found in any logical structure"); - return false; - } - - - - /** - * Gets the root logical condition that should be used for the current UI operation - */ - public LogicalCondition getFullLogicalCondition() { - // First check if there are plugin conditions - if (pluginCondition != null && !pluginCondition.getConditions().isEmpty()) { - // Need to combine user and plugin conditions with AND logic - AndCondition combinedRoot = new AndCondition(); - - // Add user logical if it has conditions - if (userLogicalCondition != null && !userLogicalCondition.getConditions().isEmpty()) { - combinedRoot.addCondition(userLogicalCondition); - // Add plugin logical - combinedRoot.addCondition(pluginCondition); - return combinedRoot; - }else { - // If no user conditions, just return plugin condition - return pluginCondition; - } - } - - // If no plugin conditions, just return user logical - return userLogicalCondition; - } - public LogicalCondition getFullLogicalUserCondition() { - return userLogicalCondition; - } - public LogicalCondition getFullLogicalPluginCondition() { - return pluginCondition; - } - - /** - * Checks if a condition is a SingleTriggerTimeCondition - * - * @param condition The condition to check - * @return true if the condition is a SingleTriggerTimeCondition - */ - private boolean isSingleTriggerCondition(Condition condition) { - return condition instanceof SingleTriggerTimeCondition; - } - public List getTriggeredOneTimeConditions(){ - List result = new ArrayList<>(); - for (Condition condition : userLogicalCondition.getConditions()) { - if (isSingleTriggerCondition(condition)) { - SingleTriggerTimeCondition singleTrigger = (SingleTriggerTimeCondition) condition; - if (singleTrigger.canTriggerAgain()) { - result.add(singleTrigger); - } - } - } - if (pluginCondition != null) { - for (Condition condition : pluginCondition.getConditions()) { - if (isSingleTriggerCondition(condition)) { - SingleTriggerTimeCondition singleTrigger = (SingleTriggerTimeCondition) condition; - if (singleTrigger.canTriggerAgain()) { - result.add(singleTrigger); - } - } - } - } - return result; - } - /** - * Checks if this condition manager contains any SingleTriggerTimeCondition that - * can no longer trigger (has already triggered) - * - * @return true if at least one single-trigger condition has already triggered - */ - public boolean hasTriggeredOneTimeConditions() { - // Check user conditions first - for (Condition condition : getUserLogicalCondition().getConditions()) { - if (isSingleTriggerCondition(condition)) { - SingleTriggerTimeCondition singleTrigger = (SingleTriggerTimeCondition) condition; - if (!singleTrigger.canTriggerAgain()) { - return true; - } - } - } - - // Then check plugin conditions if present - if (pluginCondition != null) { - for (Condition condition : pluginCondition.getConditions()) { - if (isSingleTriggerCondition(condition)) { - SingleTriggerTimeCondition singleTrigger = (SingleTriggerTimeCondition) condition; - if (!singleTrigger.canTriggerAgain()) { - return true; - } - } - } - } - - return false; - } - - /** - * Checks if the logical structure cannot trigger again due to triggered one-time conditions. - * Considers the nested AND/OR condition structure to determine if future triggering is possible. - * - * @return true if the structure cannot trigger again due to one-time conditions - */ - public boolean cannotTriggerDueToOneTimeConditions() { - // If there are no one-time conditions, the structure can always trigger again - if (!hasAnyOneTimeConditions()) { - return false; - } - - // Start evaluation at the root of the condition tree - return !canLogicalStructureTriggerAgain(getFullLogicalCondition()); - } - - /** - * Recursively evaluates if a logical structure can trigger again based on one-time conditions. - * - * @param logical The logical condition to evaluate - * @return true if the logical structure can trigger again, false otherwise - */ - private boolean canLogicalStructureTriggerAgain(LogicalCondition logical) { - if (logical instanceof AndCondition) { - // For AND logic, if any direct child one-time condition has triggered, - // the entire AND branch cannot trigger again - for (Condition condition : logical.getConditions()) { - if (condition instanceof TimeCondition) { - TimeCondition timeCondition = (TimeCondition) condition; - if (timeCondition.canTriggerAgain()) { - return false; - } - } - else if (condition instanceof ResourceCondition) { - ResourceCondition resourceCondition = (ResourceCondition) condition; - - } - else if (condition instanceof LogicalCondition) { - // Recursively check nested logic - if (!canLogicalStructureTriggerAgain((LogicalCondition) condition)) { - // If a nested branch can't trigger, this AND branch can't trigger - return false; - } - } - } - // If we get here, all branches can still trigger - return true; - } else if (logical instanceof OrCondition) { - // For OR logic, if any one-time condition hasn't triggered yet, - // the OR branch can still trigger - boolean anyCanTrigger = false; - - for (Condition child : logical.getConditions()) { - if (child instanceof TimeCondition) { - TimeCondition singleTrigger = (TimeCondition) child; - if (!singleTrigger.hasTriggered()) { - // Found an untriggered one-time condition, so this branch can trigger - return true; - } - } else if (child instanceof LogicalCondition) { - // Recursively check nested logic - if (canLogicalStructureTriggerAgain((LogicalCondition) child)) { - // If a nested branch can trigger, this OR branch can trigger - return true; - } - } else { - // Regular non-one-time conditions can always trigger - anyCanTrigger = true; - } - } - - // If there are no one-time conditions in this OR, it can trigger if it has any conditions - return anyCanTrigger; - } else { - // For any other logical condition type (e.g., NOT), assume it can trigger - return true; - } - } - /** - * Validates if the current condition structure can be triggered again - * based on the status of one-time conditions in the logical structure. - * - * @return true if the condition structure can be triggered again - */ - public boolean canTriggerAgain() { - return !cannotTriggerDueToOneTimeConditions(); - } - - /** - * Checks if this condition manager contains any SingleTriggerTimeConditions - * - * @return true if at least one single-trigger condition exists - */ - public boolean hasAnyOneTimeConditions() { - // Check user conditions - for (Condition condition : getUserLogicalCondition().getConditions()) { - if (isSingleTriggerCondition(condition)) { - return true; - } - } - - // Check plugin conditions if present - if (pluginCondition != null) { - for (Condition condition : pluginCondition.getConditions()) { - if (isSingleTriggerCondition(condition)) { - return true; - } - } - } - - return false; - } - /** - * Calculates overall progress percentage across all conditions. - * This respects the logical structure of conditions. - * Returns 0 if progress cannot be determined. - */ - private double getFullRootConditionProgress() { - // If there are no conditions, no progress to report -> nothing can be satisfied - if ( getConditions().isEmpty()) { - return 0.0; - } - - // If using logical root condition, respect its logical structure - LogicalCondition rootLogical = getFullLogicalCondition(); - if (rootLogical != null) { - return rootLogical.getProgressPercentage(); - } - - // Fallback for direct condition list: calculate based on AND/OR logic - boolean requireAll = requiresAll(); - List conditions = getConditions(); - - if (requireAll) { - // For AND logic, use the minimum progress (weakest link) - return conditions.stream() - .mapToDouble(Condition::getProgressPercentage) - .min() - .orElse(0.0); - } else { - // For OR logic, use the maximum progress (strongest link) - return conditions.stream() - .mapToDouble(Condition::getProgressPercentage) - .max() - .orElse(0.0); - } - } - /** - * Gets the overall condition progress - * Integrates both standard progress and single-trigger time conditions - */ - public double getFullConditionProgress() { - // First check for regular condition progress - double stopProgress = getFullRootConditionProgress(); - - // Then check if we have any single-trigger conditions - boolean hasOneTime = hasAnyOneTimeConditions(); - if (hasOneTime) { - // If all one-time conditions have triggered, return 100% - if (canTriggerAgain()) { - return 100.0; - } - - // If no standard progress but we have one-time conditions that haven't - // all triggered, return progress based on closest one-time condition - if (stopProgress == 0.0) { - return calculateClosestOneTimeProgress(); - } - } - - // Return the standard progress - return stopProgress; - } - - /** - * Calculates progress based on the closest one-time condition to triggering - */ - private double calculateClosestOneTimeProgress() { - // Find the single-trigger condition that's closest to triggering - double maxProgress = 0.0; - - for (Condition condition : getConditions()) { - if (condition instanceof SingleTriggerTimeCondition) { - SingleTriggerTimeCondition singleTrigger = (SingleTriggerTimeCondition) condition; - if (!singleTrigger.hasTriggered()) { - double progress = singleTrigger.getProgressPercentage(); - maxProgress = Math.max(maxProgress, progress); - } - } - } - - return maxProgress; - } - /** - * Gets the next time any condition in the structure will trigger. - * This recursively examines the logical condition tree and finds the earliest trigger time. - * If conditions are already satisfied, returns the most recent time in the past. - * - * @return Optional containing the earliest next trigger time, or empty if none available - */ - public Optional getCurrentTriggerTime() { - // Check if conditions are already met - boolean conditionsMet = areAllConditionsMet(); - if (conditionsMet) { - - - // Find the most recent trigger time in the past from all satisfied conditions - ZonedDateTime mostRecentTriggerTime = null; - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - - // Recursively scan all conditions that are satisfied - for (Condition condition : getConditions()) { - if (condition.isSatisfied()) { - Optional conditionTrigger = condition.getCurrentTriggerTime(); - if (conditionTrigger.isPresent()) { - ZonedDateTime triggerTime = conditionTrigger.get(); - - // Only consider times in the past - if (triggerTime.isBefore(now) || triggerTime.isEqual(now)) { - // Keep the most recent time in the past - if (mostRecentTriggerTime == null || triggerTime.isAfter(mostRecentTriggerTime)) { - mostRecentTriggerTime = triggerTime; - } - } - } - } - } - - // If we found a trigger time from satisfied conditions, return it - if (mostRecentTriggerTime != null) { - return Optional.of(mostRecentTriggerTime); - } - - // If no trigger times found from satisfied conditions, default to immediate past - ZonedDateTime immediateTime = now.minusSeconds(1); - return Optional.of(immediateTime); - } - - // Otherwise proceed with normal logic for finding next trigger time - log.debug("Conditions not yet met, searching for next trigger time in logical structure"); - Optional nextTime = getCurrentTriggerTimeForLogical(getFullLogicalCondition()); - - if (nextTime.isPresent()) { - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - ZonedDateTime triggerTime = nextTime.get(); - - if (triggerTime.isBefore(now)) { - log.debug("Found trigger time {} is in the past compared to now {}", - triggerTime, now); - - // If trigger time is in the past but conditions aren't met, - // this might indicate a condition that needs resetting - if (!conditionsMet) { - log.debug("Trigger time in past but conditions not met - may need reset"); - } - } else { - log.debug("Found future trigger time: {}", triggerTime); - } - } else { - log.debug("No trigger time found in condition structure"); - } - - return nextTime; - } - - /** - * Recursively finds the appropriate trigger time within a logical condition. - * - For conditions not yet met: finds the earliest future trigger time - * - For conditions already met: finds the most recent past trigger time - * - * @param logical The logical condition to examine - * @return Optional containing the appropriate trigger time, or empty if none available - */ - private Optional getCurrentTriggerTimeForLogical(LogicalCondition logical) { - if (logical == null || logical.getConditions().isEmpty()) { - log.debug("Logical condition is null or empty, no trigger time available"); - return Optional.empty(); - } - - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - - // If the logical condition is already satisfied, find most recent past trigger time - if (logical.isSatisfied()) { - ZonedDateTime mostRecentTriggerTime = null; - - for (Condition condition : logical.getConditions()) { - if (condition.isSatisfied()) { - log.debug("Checking past trigger time for satisfied condition: {}", condition.getDescription()); - Optional triggerTime; - - if (condition instanceof LogicalCondition) { - // Recursively check nested logical conditions - triggerTime = getCurrentTriggerTimeForLogical((LogicalCondition) condition); - } else { - // Get trigger time from individual condition - triggerTime = condition.getCurrentTriggerTime(); - } - - if (triggerTime.isPresent()) { - ZonedDateTime time = triggerTime.get(); - // Only consider times in the past - if (time.isBefore(now) || time.isEqual(now)) { - // Keep the most recent time in the past - if (mostRecentTriggerTime == null || time.isAfter(mostRecentTriggerTime)) { - mostRecentTriggerTime = time; - log.debug("Found more recent past trigger time: {}", mostRecentTriggerTime); - } - } - } - } - } - - if (mostRecentTriggerTime != null) { - return Optional.of(mostRecentTriggerTime); - } - } - - // If not satisfied, find earliest future trigger time (original behavior) - ZonedDateTime earliestTrigger = null; - - for (Condition condition : logical.getConditions()) { - log.debug("Checking next trigger time for condition: {}", condition.getDescription()); - Optional nextTrigger; - - if (condition instanceof LogicalCondition) { - // Recursively check nested logical conditions - log.debug("Recursing into nested logical condition"); - nextTrigger = getCurrentTriggerTimeForLogical((LogicalCondition) condition); - } else { - // Get trigger time from individual condition - nextTrigger = condition.getCurrentTriggerTime(); - log.debug("Condition {} trigger time: {}", - condition.getDescription(), - nextTrigger.isPresent() ? nextTrigger.get() : "none"); - } - - // Update earliest trigger if this one is earlier - if (nextTrigger.isPresent()) { - ZonedDateTime triggerTime = nextTrigger.get(); - if (earliestTrigger == null || triggerTime.isBefore(earliestTrigger)) { - log.debug("Found earlier trigger time: {}", triggerTime); - earliestTrigger = triggerTime; - } - } - } - - if (earliestTrigger != null) { - log.debug("Earliest trigger time for logical condition: {}", earliestTrigger); - return Optional.of(earliestTrigger); - } else { - log.debug("No trigger times found in logical condition"); - return Optional.empty(); - } - } - - /** - * Gets the next time any condition in the structure will trigger. - * This recursively examines the logical condition tree and finds the earliest trigger time. - * - * @return Optional containing the earliest next trigger time, or empty if none available - */ - public Optional getCurrentTriggerTimeBasedOnUserConditions() { - // Start at the root of the condition tree - return getCurrentTriggerTimeForLogical(getFullLogicalUserCondition()); - } - /** - * Determines the next time a plugin should be triggered based on the plugin's set conditions. - * This method evaluates the full logical condition tree for the plugin to calculate - * when the next condition-based execution should occur. - * - * @return An Optional containing the ZonedDateTime of the next trigger time if one exists, - * or an empty Optional if no future trigger time can be determined - */ - public Optional getCurrentTriggerTimeBasedOnPluginConditions() { - // Start at the root of the condition tree - return getCurrentTriggerTimeForLogical(getFullLogicalPluginCondition()); - } - - /** - * Gets the duration until the next condition trigger. - * For conditions already satisfied, returns Duration.ZERO. - * - * @return Optional containing the duration until next trigger, or empty if none available - */ - public Optional getDurationUntilNextTrigger() { - // If conditions are already met, return zero duration - if (areAllConditionsMet()) { - return Optional.of(Duration.ZERO); - } - - Optional nextTrigger = getCurrentTriggerTime(); - if (nextTrigger.isPresent()) { - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - ZonedDateTime triggerTime = nextTrigger.get(); - - // If trigger time is in the future, return the duration - if (triggerTime.isAfter(now)) { - return Optional.of(Duration.between(now, triggerTime)); - } - - // If trigger time is in the past but conditions aren't met, - // this indicates a condition that needs resetting - log.debug("Trigger time in past but conditions not met - returning zero duration"); - return Optional.of(Duration.ZERO); - } - return Optional.empty(); - } - - /** - * Formats the next trigger time as a human-readable string. - * - * @return A string representing when the next condition will trigger, or "No upcoming triggers" if none - */ - public String getCurrentTriggerTimeString() { - Optional nextTrigger = getCurrentTriggerTime(); - if (nextTrigger.isPresent()) { - ZonedDateTime triggerTime = nextTrigger.get(); - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - - // Format nicely depending on how far in the future - Duration timeUntil = Duration.between(now, triggerTime); - long seconds = timeUntil.getSeconds(); - - if (seconds < 0) { - return "Already triggered"; - } else if (seconds < 60) { - return String.format("Triggers in %d seconds", seconds); - } else if (seconds < 3600) { - return String.format("Triggers in %d minutes %d seconds", - seconds / 60, seconds % 60); - } else if (seconds < 86400) { // Less than a day - return String.format("Triggers in %d hours %d minutes", - seconds / 3600, (seconds % 3600) / 60); - } else { - // More than a day away, use date format - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM d 'at' HH:mm"); - return "Triggers on " + triggerTime.format(formatter); - } - } - - return "No upcoming triggers"; - } - - /** - * Gets the overall progress percentage toward the next trigger time. - * For logical conditions, this respects the logical structure. - * - * @return Progress percentage (0-100) - */ - public double getProgressTowardNextTrigger() { - LogicalCondition rootLogical = getFullLogicalCondition(); - - if (rootLogical instanceof AndCondition) { - // For AND logic, use the minimum progress (we're only as close as our furthest condition) - return rootLogical.getConditions().stream() - .mapToDouble(Condition::getProgressPercentage) - .min() - .orElse(0.0); - } else { - // For OR logic, use the maximum progress (we're as close as our closest condition) - return rootLogical.getConditions().stream() - .mapToDouble(Condition::getProgressPercentage) - .max() - .orElse(0.0); - } - } - @Subscribe(priority = -1) - public void onGameStateChanged(GameStateChanged gameStateChanged){ - for (Condition condition : getConditions( )) { - try { - condition.onGameStateChanged(gameStateChanged); - } catch (Exception e) { - log.error("Error in condition {} during GameStateChanged event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - - @Subscribe(priority = -1) - public void onStatChanged(StatChanged event) { - for (Condition condition : getConditions( )) { - try { - condition.onStatChanged(event); - } catch (Exception e) { - log.error("Error in condition {} during StatChanged event: {}", - condition.getDescription(), e.getMessage(), e); - e.printStackTrace(); - } - } - } - - - @Subscribe(priority = -1) - public void onItemContainerChanged(ItemContainerChanged event) { - // Propagate event to all conditions - for (Condition condition : getConditions( )) { - try { - condition.onItemContainerChanged(event); - } catch (Exception e) { - log.error("Error in condition {} during ItemContainerChanged event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - @Subscribe(priority = -1) - public void onGameTick(GameTick gameTick) { - // Propagate event to all conditions - for (Condition condition : getConditions( )) { - try { - condition.onGameTick(gameTick); - } catch (Exception e) { - log.error("Error in condition {} during GameTick event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - - @Subscribe(priority = -1) - public void onGroundObjectSpawned(GroundObjectSpawned event) { - // Propagate event to all conditions - for (Condition condition : getConditions( )) { - try { - condition.onGroundObjectSpawned(event); - } catch (Exception e) { - log.error("Error in condition {} during GroundItemSpawned event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - if (pluginCondition != null) { - try { - pluginCondition.onGroundObjectSpawned(event); - } catch (Exception e) { - log.error("Error in plugin condition during GroundItemSpawned event: {}", - e.getMessage(), e); - } - } - } - - @Subscribe(priority = -1) - public void onGroundObjectDespawned(GroundObjectDespawned event) { - for (Condition condition : getConditions( )) { - try { - condition.onGroundObjectDespawned(event); - } catch (Exception e) { - log.error("Error in condition {} during GroundItemDespawned event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - - @Subscribe(priority = -1) - public void onMenuOptionClicked(MenuOptionClicked event) { - for (Condition condition : getConditions( )) { - try { - condition.onMenuOptionClicked(event); - } catch (Exception e) { - log.error("Error in condition {} during MenuOptionClicked event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - - @Subscribe(priority = -1) - public void onChatMessage(ChatMessage event) { - for (Condition condition : getConditions( )) { - try { - condition.onChatMessage(event); - } catch (Exception e) { - log.error("Error in condition {} during ChatMessage event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - - @Subscribe(priority = -1) - public void onHitsplatApplied(HitsplatApplied event) { - for (Condition condition : getConditions( )) { - try { - condition.onHitsplatApplied(event); - } catch (Exception e) { - log.error("Error in condition {} during HitsplatApplied event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - - } - @Subscribe(priority = -1) - public void onVarbitChanged(VarbitChanged event) - { - for (Condition condition :getConditions( )) { - try { - condition.onVarbitChanged(event); - } catch (Exception e) { - log.error("Error in condition {} during VarbitChanged event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - @Subscribe(priority = -1) - void onNpcChanged(NpcChanged event){ - for (Condition condition :getConditions( )) { - try { - condition.onNpcChanged(event); - } catch (Exception e) { - log.error("Error in condition {} during NpcChanged event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - @Subscribe(priority = -1) - void onNpcSpawned(NpcSpawned npcSpawned){ - for (Condition condition : getConditions( )) { - try { - condition.onNpcSpawned(npcSpawned); - } catch (Exception e) { - log.error("Error in condition {} during NpcSpawned event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - - } - @Subscribe(priority = -1) - void onNpcDespawned(NpcDespawned npcDespawned){ - for (Condition condition : getConditions( )) { - try { - condition.onNpcDespawned(npcDespawned); - } catch (Exception e) { - log.error("Error in condition {} during NpcDespawned event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - @Subscribe(priority = -1) - void onInteractingChanged(InteractingChanged event){ - for (Condition condition : getConditions( )) { - try { - condition.onInteractingChanged(event); - } catch (Exception e) { - log.error("Error in condition {} during InteractingChanged event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - @Subscribe(priority = -1) - void onItemSpawned(ItemSpawned event){ - List allConditions = getConditions(); - // Proceed with normal processing - for (Condition condition : allConditions) { - try { - condition.onItemSpawned(event); - } catch (Exception e) { - log.error("Error in condition {} during ItemSpawned event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - @Subscribe(priority = -1) - void onItemDespawned(ItemDespawned event){ - for (Condition condition :getConditions( )) { - try { - condition.onItemDespawned(event); - } catch (Exception e) { - log.error("Error in condition {} during ItemDespawned event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - @Subscribe(priority = -1) - void onAnimationChanged(AnimationChanged event) { - for (Condition condition :getConditions( )) { - try { - condition.onAnimationChanged(event); - } catch (Exception e) { - log.error("Error in condition {} during AnimationChanged event: {}", - condition.getDescription(), e.getMessage(), e); - } - } - } - - /** - * Finds the logical condition that contains the given condition - * - * @param targetCondition The condition to find - * @return The logical condition containing it, or null if not found - */ - public LogicalCondition findContainingLogical(Condition targetCondition) { - // First check if it's in the plugin condition - if (pluginCondition != null && findInLogical(pluginCondition, targetCondition) != null) { - return findInLogical(pluginCondition, targetCondition); - } - - // Then check user logical condition - if (userLogicalCondition != null) { - LogicalCondition result = findInLogical(userLogicalCondition, targetCondition); - if (result != null) { - return result; - } - } - - // Try root logical condition as a last resort - return userLogicalCondition; - } - - /** - * Recursively searches for a condition within a logical condition - */ - private LogicalCondition findInLogical(LogicalCondition logical, Condition targetCondition) { - // Check if the condition is directly in this logical - if (logical.getConditions().contains(targetCondition)) { - return logical; - } - - // Check nested logical conditions - for (Condition condition : logical.getConditions()) { - if (condition instanceof LogicalCondition) { - LogicalCondition result = findInLogical((LogicalCondition) condition, targetCondition); - if (result != null) { - return result; - } - } - } - - return null; - } - - /** - * Creates a new ConditionManager that contains only the time conditions from this manager, - * preserving the logical structure hierarchy (AND/OR relationships). - * - * @return A new ConditionManager with only time conditions, or null if no time conditions exist - */ - public ConditionManager createTimeOnlyConditionManager() { - // Create a new condition manager - ConditionManager timeOnlyManager = new ConditionManager(); - - // Process user logical condition - if (userLogicalCondition != null) { - LogicalCondition timeOnlyUserLogical = userLogicalCondition.createTimeOnlyLogicalStructure(); - if (timeOnlyUserLogical != null) { - timeOnlyManager.setUserLogicalCondition(timeOnlyUserLogical); - } - } - - // Process plugin logical condition - if (pluginCondition != null) { - LogicalCondition timeOnlyPluginLogical = pluginCondition.createTimeOnlyLogicalStructure(); - if (timeOnlyPluginLogical != null) { - timeOnlyManager.setPluginCondition(timeOnlyPluginLogical); - } - } - - return timeOnlyManager; - } - - /** - * Evaluates whether this condition manager would be satisfied if only time conditions - * were considered. This is useful to determine if a plugin schedule is blocked by - * non-time conditions or by the time conditions themselves. - * - * @return true if time conditions alone would satisfy this condition manager, false otherwise - */ - public boolean wouldBeTimeOnlySatisfied() { - // If there are no time conditions at all, we can't satisfy with time only - List timeConditions = getTimeConditions(); - if (timeConditions.isEmpty()) { - return false; - } - - boolean userConditionsSatisfied = false; - boolean pluginConditionsSatisfied = false; - - // Check user logical condition - if (userLogicalCondition != null) { - userConditionsSatisfied = userLogicalCondition.wouldBeTimeOnlySatisfied(); - } - - // Check plugin logical condition - if (pluginCondition != null && !pluginCondition.getConditions().isEmpty()) { - pluginConditionsSatisfied = pluginCondition.wouldBeTimeOnlySatisfied(); - - // Both user and plugin conditions must be satisfied (AND logic between them) - return userConditionsSatisfied && pluginConditionsSatisfied; - } - - // If no plugin conditions, just return user conditions result - return userConditionsSatisfied; - } - - /** - * Evaluates if only the time conditions in both user and plugin logical structures would be met. - * This method provides more detailed diagnostics than wouldBeTimeOnlySatisfied(). - * - * @return A string containing diagnostic information about time condition satisfaction - */ - public String diagnoseTimeConditionsSatisfaction() { - StringBuilder sb = new StringBuilder("Time conditions diagnosis:\n"); - - // Get all time conditions - List timeConditions = getTimeConditions(); - - // Check if there are any time conditions - if (timeConditions.isEmpty()) { - sb.append("No time conditions defined - cannot be satisfied based on time only.\n"); - return sb.toString(); - } - - // Check user logical time conditions - if (userLogicalCondition != null) { - boolean userTimeOnlySatisfied = userLogicalCondition.wouldBeTimeOnlySatisfied(); - sb.append("User time conditions: ").append(userTimeOnlySatisfied ? "SATISFIED" : "NOT SATISFIED").append("\n"); - - // List each time condition in user logical - List userTimeConditions = userLogicalCondition.findTimeConditions(); - if (!userTimeConditions.isEmpty()) { - sb.append(" User time conditions (").append(userLogicalCondition instanceof AndCondition ? "ALL" : "ANY").append(" required):\n"); - for (Condition condition : userTimeConditions) { - boolean satisfied = condition.isSatisfied(); - sb.append(" - ").append(condition.getDescription()) - .append(": ").append(satisfied ? "SATISFIED" : "NOT SATISFIED") - .append("\n"); - } - } - } - - // Check plugin logical time conditions - if (pluginCondition != null && !pluginCondition.getConditions().isEmpty()) { - boolean pluginTimeOnlySatisfied = pluginCondition.wouldBeTimeOnlySatisfied(); - sb.append("Plugin time conditions: ").append(pluginTimeOnlySatisfied ? "SATISFIED" : "NOT SATISFIED").append("\n"); - - // List each time condition in plugin logical - List pluginTimeConditions = pluginCondition.findTimeConditions(); - if (!pluginTimeConditions.isEmpty()) { - sb.append(" Plugin time conditions (").append(pluginCondition instanceof AndCondition ? "ALL" : "ANY").append(" required):\n"); - for (Condition condition : pluginTimeConditions) { - boolean satisfied = condition.isSatisfied(); - sb.append(" - ").append(condition.getDescription()) - .append(": ").append(satisfied ? "SATISFIED" : "NOT SATISFIED") - .append("\n"); - } - } - } - - // Overall result - boolean overallTimeOnlySatisfied = wouldBeTimeOnlySatisfied(); - sb.append("Overall result: ").append(overallTimeOnlySatisfied ? - "Would be SATISFIED based on time conditions only" : - "Would NOT be satisfied based on time conditions only"); - - return sb.toString(); - } - - - - - - /** - * Updates the plugin condition structure with new conditions from the given logical condition. - * This method intelligently merges the new conditions into the existing structure - * rather than replacing it completely, which preserves state and reduces unnecessary - * condition reinitializations. - * - * @param newPluginCondition The new logical condition to merge into the existing plugin condition - * @return true if changes were made to the plugin condition, false otherwise - */ - public boolean updatePluginCondition(LogicalCondition newPluginCondition) { - // Use the default update option (ADD_ONLY) - return updatePluginCondition(newPluginCondition, UpdateOption.SYNC); - } - - /** - * Updates the plugin condition structure with new conditions from the given logical condition. - * This method provides fine-grained control over how conditions are merged. - * - * @param newPluginCondition The new logical condition to merge into the existing plugin condition - * @param updateOption Controls how conditions are merged - * @return true if changes were made to the plugin condition, false otherwise - */ - public boolean updatePluginCondition(LogicalCondition newPluginCondition, UpdateOption updateOption) { - return updatePluginCondition(newPluginCondition, updateOption, true); - } - - /** - * Updates the plugin condition structure with new conditions from the given logical condition. - * This method provides complete control over how conditions are merged. - * - * @param newPluginCondition The new logical condition to merge into the existing plugin condition - * @param updateOption Controls how conditions are merged - * @param preserveState If true, existing condition state is preserved when possible - * @return true if changes were made to the plugin condition, false otherwise - */ - public boolean updatePluginCondition(LogicalCondition newPluginCondition, UpdateOption updateOption, boolean preserveState) { - if (newPluginCondition == null) { - return false; - } - - // If we don't have a plugin condition yet, just set it directly - if (pluginCondition == null) { - setPluginCondition(newPluginCondition); - log.debug("Initialized plugin condition from null"); - return true; - } - - // Create a copy of the new condition and optimize it before comparison - // This ensures both structures are in optimized form when comparing - LogicalCondition optimizedNewCondition; - if (newPluginCondition instanceof AndCondition) { - optimizedNewCondition = new AndCondition(); - } else { - optimizedNewCondition = new OrCondition(); - } - - // Copy all conditions from the new structure to the optimized copy - for (Condition condition : newPluginCondition.getConditions()) { - optimizedNewCondition.addCondition(condition); - } - - // Optimize the new structure to match how the existing one is optimized - optimizedNewCondition.optimizeStructure(); - - if (!optimizedNewCondition.equals(pluginCondition)) { - StringBuilder sb = new StringBuilder(); - sb.append("\nNew Plugin Condition Detected:\n"); - sb.append("newPluginCondition: \n\n\t").append(optimizedNewCondition.getDescription()).append("\n\n"); - sb.append("pluginCondition: \n\n\t").append(pluginCondition.getDescription()).append("\n\n"); - sb.append("Differences: \n\t").append(pluginCondition.getStructureDifferences(optimizedNewCondition)); - log.debug(sb.toString()); - - } - - // If the logical types don't match (AND vs OR), and we're not in REPLACE mode, - // we need special handling - boolean typeMismatch = - (pluginCondition instanceof AndCondition && !(newPluginCondition instanceof AndCondition)) || - (pluginCondition instanceof OrCondition && !(newPluginCondition instanceof OrCondition)); - - // Use the rest of the existing function logic - if (typeMismatch) { - if (updateOption == UpdateOption.REPLACE) { - // For REPLACE, just replace the entire condition - log.debug("Replacing plugin condition due to logical type mismatch: {} -> {}", - pluginCondition.getClass().getSimpleName(), - newPluginCondition.getClass().getSimpleName()); - setPluginCondition(newPluginCondition); - return true; - } else if (updateOption == UpdateOption.SYNC) { - // For SYNC with type mismatch, log a warning but try to merge anyway - log.debug("\nAttempting to synchronize plugin conditions with different logical types: {} ({})-> {} ({})", - pluginCondition.getClass().getSimpleName(),pluginCondition.getConditions().size(), - newPluginCondition.getClass().getSimpleName(),newPluginCondition.getConditions().size()); - // Continue with sync by creating a new condition of the correct type - LogicalCondition newRootCondition; - if (newPluginCondition instanceof AndCondition) { - newRootCondition = new AndCondition(); - } else { - newRootCondition = new OrCondition(); - } - - // Copy all conditions from the old structure that also appear in the new one - for (Condition existingCond : pluginCondition.getConditions()) { - if (newPluginCondition.contains(existingCond)) { - newRootCondition.addCondition(existingCond); - } - } - - // Add any new conditions from the new structure - for (Condition newCond : newPluginCondition.getConditions()) { - if (!newRootCondition.contains(newCond)) { - newRootCondition.addCondition(newCond); - } - } - - setPluginCondition(newRootCondition); - return true; - } - } - - // Use the LogicalCondition's updateLogicalStructure method with the specified options - boolean conditionsUpdated = pluginCondition.updateLogicalStructure( - newPluginCondition, - updateOption, - preserveState); - - if (!optimizedNewCondition.equals(pluginCondition)) { - StringBuilder sb = new StringBuilder(); - sb.append("Plugin condition updated with option -> difference should not occur: \nObjection:\t").append(updateOption).append("\n"); - sb.append("New Plugin Condition Detected:\n"); - sb.append("newPluginCondition: \n\n").append(optimizedNewCondition.getDescription()).append("\n\n"); - sb.append("pluginCondition: \n\n").append(pluginCondition.getDescription()).append("\n\n"); - sb.append("Differences: should not exist: \n\t").append(pluginCondition.getStructureDifferences(optimizedNewCondition)); - log.warn(sb.toString()); - } - - // Optimize the condition structure after update if needed - if (conditionsUpdated && updateOption != UpdateOption.REMOVE_ONLY) { - // Optimize only if we added or changed conditions - boolean optimized = pluginCondition.optimizeStructure(); - if (optimized) { - log.info("Optimized plugin condition structure after update!! \n new plugin condition: \n\n" + pluginCondition); - - } - - // Validate the structure - List issues = pluginCondition.validateStructure(); - if (!issues.isEmpty()) { - log.warn("Validation issues found in plugin condition structure:"); - for (String issue : issues) { - log.warn(" - {}", issue); - } - } - } - - if (conditionsUpdated) { - log.debug("Updated plugin condition structure, changes were applied"); - - if (log.isTraceEnabled()) { - String differences = pluginCondition.getStructureDifferences(newPluginCondition); - log.trace("Condition structure differences after update:\n{}", differences); - } - } else { - log.debug("No changes needed to plugin condition structure"); - } - - return conditionsUpdated; - } - - - - /** - * Clean up resources when this condition manager is no longer needed. - * Implements the AutoCloseable interface for proper resource management. - */ - @Override - public void close() { - // Unregister from events to prevent memory leaks - unregisterEvents(); - - // Cancel all scheduled watchdog tasks - cancelAllWatchdogs(); - - log.debug("ConditionManager resources cleaned up"); - } - - /** - * Cancels all watchdog tasks scheduled by this condition manager. - */ - public void cancelAllWatchdogs() { - synchronized (watchdogFutures) { - for (ScheduledFuture> future : watchdogFutures) { - if (future != null && !future.isDone()) { - future.cancel(false); - } - } - watchdogFutures.clear(); - } - watchdogsRunning = false; - } - - /** - * Shutdowns the shared watchdog executor service. - * This should only be called when the application is shutting down. - */ - public static void shutdownSharedExecutor() { - if (!SHARED_WATCHDOG_EXECUTOR.isShutdown()) { - SHARED_WATCHDOG_EXECUTOR.shutdown(); - try { - if (!SHARED_WATCHDOG_EXECUTOR.awaitTermination(1, TimeUnit.SECONDS)) { - SHARED_WATCHDOG_EXECUTOR.shutdownNow(); - } - } catch (InterruptedException e) { - SHARED_WATCHDOG_EXECUTOR.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - } - - /** - * Periodically checks the plugin condition structure against a new condition structure - * and updates if necessary. This creates a scheduled task that runs at the specified interval. - * Uses the default ADD_ONLY update option. - * - * @param conditionSupplier A supplier that returns the current desired plugin condition - * @param interval The interval in milliseconds between checks - * @return A handle to the scheduled future task (can be used to cancel) - */ - public ScheduledFuture> scheduleConditionWatchdog( - java.util.function.Supplier conditionSupplier, - long interval) { - - return scheduleConditionWatchdog(conditionSupplier, interval, UpdateOption.SYNC); - } - - /** - * Periodically checks the plugin condition structure against a new condition structure - * and updates if necessary. This creates a scheduled task that runs at the specified interval. - * - * @param conditionSupplier A supplier that returns the current desired plugin condition - * @param interval The interval in milliseconds between checks - * @param updateOption The update option to use for condition changes - * @return A handle to the scheduled future task (can be used to cancel) - */ - public ScheduledFuture> scheduleConditionWatchdog( - java.util.function.Supplier conditionSupplier, - long interval, - UpdateOption updateOption) { - if(areWatchdogsRunning() ) { - - log.debug("Watchdogs were already running, cancelling all before starting new ones"); - } - if (!watchdogFutures.isEmpty()) { - watchdogFutures.clear(); - } - - // Store the configuration for possible later resume - currentWatchdogSupplier = conditionSupplier; - currentWatchdogInterval = interval; - currentWatchdogUpdateOption = updateOption; - - ScheduledFuture> future = SHARED_WATCHDOG_EXECUTOR.scheduleWithFixedDelay(() -> { //scheduleWithFixedRate - try { - // First cleanup any non-triggerable conditions from existing structures - boolean cleanupDone = cleanupNonTriggerableConditions(); - if (cleanupDone) { - log.debug("Watchdog removed non-triggerable conditions from logical structures"); - } - - // Then update with new conditions - LogicalCondition currentDesiredCondition = conditionSupplier.get(); - if (currentDesiredCondition == null) { - currentDesiredCondition = new OrCondition(); - } - - // Clean non-triggerable conditions from the new condition structure too - if (currentDesiredCondition != null) { - LogicalCondition.removeNonTriggerableConditions(currentDesiredCondition); - - boolean updated = updatePluginCondition(currentDesiredCondition, updateOption); - if (updated) { - log.debug("Watchdog updated plugin conditions using mode: {}", updateOption); - } - } - } catch (Exception e) { - log.error("Error in plugin condition watchdog", e); - } - }, 0, interval, TimeUnit.MILLISECONDS); - - // Track this future for proper cleanup - synchronized (watchdogFutures) { - watchdogFutures.add(future); - } - - watchdogsRunning = true; - return future; - } - - /** - * Checks if watchdogs are currently running for this condition manager. - * - * @return true if at least one watchdog is active - */ - public boolean areWatchdogsRunning() { - if (!watchdogsRunning) { - return false; - } - - // Double-check by examining futures - synchronized (watchdogFutures) { - if (watchdogFutures.isEmpty()) { - watchdogsRunning = false; - return false; - } - - // Check if at least one watchdog is active - for (ScheduledFuture> future : watchdogFutures) { - if (future != null && !future.isDone() && !future.isCancelled()) { - return true; - } - } - - // No active watchdogs found - watchdogsRunning = false; - return false; - } - } - - /** - * Pauses all watchdog tasks without removing them completely. - * This allows them to be resumed later with the same settings. - * - * @return true if watchdogs were successfully paused - */ - public boolean pauseWatchdogs() { - if (!watchdogsRunning) { - return false; // Nothing to pause - } - - cancelAllWatchdogs(); - watchdogsRunning = false; - log.debug("Watchdogs paused"); - return true; - } - - /** - * Resumes watchdogs that were previously paused with the same configuration. - * If no watchdog was previously configured, this does nothing. - * - * @return true if watchdogs were successfully resumed - */ - public boolean resumeWatchdogs() { - if (watchdogsRunning || currentWatchdogSupplier == null) { - return false; // Already running or never configured - } - - scheduleConditionWatchdog( - currentWatchdogSupplier, - currentWatchdogInterval, - currentWatchdogUpdateOption - ); - - watchdogsRunning = true; - log.debug("Watchdogs resumed with interval {}ms and update option {}", - currentWatchdogInterval, currentWatchdogUpdateOption); - return true; - } - - /** - * Registers events and starts watchdogs in one call. - * If watchdogs are already configured but paused, this will resume them. - * - * @param conditionSupplier The supplier for conditions - * @param intervalMillis The interval for watchdog checks - * @param updateOption How to update conditions - * @return true if successfully started - */ - public boolean registerEventsAndStartWatchdogs( - Supplier conditionSupplier, - long intervalMillis, - UpdateOption updateOption) { - - // Register for events first - registerEvents(); - - // Then setup watchdogs - if (watchdogsRunning) { - // If already running with different settings, restart - pauseWatchdogs(); - } - - // Store current configuration - currentWatchdogSupplier = conditionSupplier; - currentWatchdogInterval = intervalMillis; - currentWatchdogUpdateOption = updateOption; - - // Start the watchdogs - scheduleConditionWatchdog(conditionSupplier, intervalMillis, updateOption); - watchdogsRunning = true; - - return true; - } - - /** - * Unregisters events and pauses watchdogs in one call. - */ - public void unregisterEventsAndPauseWatchdogs() { - unregisterEvents(); - pauseWatchdogs(); - } - - /** - * Cleans up non-triggerable conditions from both plugin and user logical structures. - * This is useful to call periodically to keep the condition structures streamlined. - * - * @return true if any conditions were removed, false otherwise - */ - public boolean cleanupNonTriggerableConditions() { - boolean anyRemoved = false; - - // Clean up plugin conditions - if (pluginCondition != null) { - anyRemoved = LogicalCondition.removeNonTriggerableConditions(pluginCondition) || anyRemoved; - } - - // Clean up user conditions - if (userLogicalCondition != null) { - anyRemoved = LogicalCondition.removeNonTriggerableConditions(userLogicalCondition) || anyRemoved; - } - - return anyRemoved; - } - - /** - * Gets a list of all plugin-defined conditions that are currently blocking the plugin from running. - * - * @return List of all plugin-defined conditions preventing activation - */ - public List getPluginBlockingConditions() { - List blockingConditions = new ArrayList<>(); - - if (pluginCondition != null && !pluginCondition.isSatisfied() && pluginCondition instanceof LogicalCondition) { - blockingConditions.addAll(((LogicalCondition) pluginCondition).getBlockingConditions()); - } - - return blockingConditions; - } - - /** - * Gets a list of all user-defined conditions that are currently blocking the plugin from running. - * - * @return List of all user-defined conditions preventing activation - */ - public List